80 | ```
81 |
82 | Breaking Change section should start with the phrase "BREAKING CHANGE: " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
83 |
84 | Similarly, a Deprecation section should start with "DEPRECATED: " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
85 |
86 | [Angular commit message]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit-message-format
87 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022, Dylan Jones
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LattPy - Simple Lattice Modeling in Python
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | :warning: **WARNING**: This project is still in development and might change significantly in the future!
18 |
19 | *LattPy* is a simple and efficient Python package for modeling Bravais lattices and
20 | constructing (finite) lattice structures in any dimension.
21 | It provides an easy interface for constructing lattice structures by simplifying the
22 | configuration of the unit cell and the neighbor connections - making it possible to
23 | construct complex models in just a few lines of code and without the headache of
24 | adding neighbor connections manually. You will save time and mental energy for more important matters.
25 |
26 | | Master | [![Test][tests-master]][link-tests] | [![Codecov][codecov-master]][codecov-master-link] | [![Read the Docs][docs-master]][docs-master-link] |
27 | |:-------|:------------------------------------|:--------------------------------------------------|:--------------------------------------------------|
28 | | Dev | [![Test][tests-dev]][link-tests] | [![Codecov][codecov-dev]][codecov-dev-link] | [![Read the Docs][docs-dev]][docs-dev-link] |
29 |
30 |
31 | ## 🔧 Installation
32 |
33 | LattPy is available on [PyPI](https://pypi.org/project/lattpy/):
34 | ````commandline
35 | pip install lattpy
36 | ````
37 |
38 | Alternatively, it can be installed via [GitHub](https://github.com/dylanljones/lattpy)
39 | ```commandline
40 | pip install git+https://github.com/dylanljones/lattpy.git@VERSION
41 | ```
42 | where `VERSION` is a release or tag. The project can also be
43 | cloned/forked and installed via
44 | ````commandline
45 | python setup.py install
46 | ````
47 |
48 | ## 📖 Documentation
49 |
50 | [Read the documentation on ReadTheDocs!][docs-stable-link]
51 |
52 | ## 🚀 Quick-Start
53 |
54 | See the [tutorial][docs-tutorial-link] for more information and examples.
55 |
56 | Features:
57 |
58 | - Basis transformations
59 | - Configurable unit cell
60 | - Easy neighbor configuration
61 | - General lattice structures
62 | - Finite lattice models in world or lattice coordinates
63 | - Periodic boundary conditions along any axis
64 |
65 | ### Configuration
66 |
67 | A new instance of a lattice model is initialized using the unit-vectors of the Bravais lattice.
68 | After the initialization the atoms of the unit-cell need to be added. To finish the configuration
69 | the connections between the atoms in the lattice have to be set. This can either be done for
70 | each atom-pair individually by calling ``add_connection`` or for all possible pairs at once by
71 | callling ``add_connections``. The argument is the number of unique
72 | distances of neighbors. Setting a value of ``1`` will compute only the nearest
73 | neighbors of the atom.
74 | ````python
75 | import numpy as np
76 | from lattpy import Lattice
77 |
78 | latt = Lattice(np.eye(2)) # Construct a Bravais lattice with square unit-vectors
79 | latt.add_atom(pos=[0.0, 0.0]) # Add an Atom to the unit cell of the lattice
80 | latt.add_connections(1) # Set the maximum number of distances between all atoms
81 |
82 | latt = Lattice(np.eye(2)) # Construct a Bravais lattice with square unit-vectors
83 | latt.add_atom(pos=[0.0, 0.0], atom="A") # Add an Atom to the unit cell of the lattice
84 | latt.add_atom(pos=[0.5, 0.5], atom="B") # Add an Atom to the unit cell of the lattice
85 | latt.add_connection("A", "A", 1) # Set the max number of distances between A and A
86 | latt.add_connection("A", "B", 1) # Set the max number of distances between A and B
87 | latt.add_connection("B", "B", 1) # Set the max number of distances between B and B
88 | latt.analyze()
89 | ````
90 |
91 | Configuring all connections using the ``add_connections``-method will call the ``analyze``-method
92 | directly. Otherwise this has to be called at the end of the lattice setup or by using
93 | ``analyze=True`` in the last call of ``add_connection``. This will compute the number of neighbors,
94 | their distances and their positions for each atom in the unitcell.
95 |
96 | To speed up the configuration prefabs of common lattices are included. The previous lattice
97 | can also be created with
98 | ````python
99 | from lattpy import simple_square
100 |
101 | latt = simple_square(a=1.0, neighbors=1) # Initializes a square lattice with one atom in the unit-cell
102 | ````
103 |
104 | So far only the lattice structure has been configured. To actually construct a (finite) model of the lattice
105 | the model has to be built:
106 | ````python
107 | latt.build(shape=(5, 3))
108 | ````
109 | This will compute the indices and neighbors of all sites in the given shape and store the data.
110 |
111 | After building the lattice periodic boundary conditions can be set along one or multiple axes:
112 | ````python
113 | latt.set_periodic(axis=0)
114 | ````
115 |
116 | To view the built lattice the `plot`-method can be used:
117 | ````python
118 | import matplotlib.pyplot as plt
119 |
120 | latt.plot()
121 | plt.show()
122 | ````
123 |
124 |
125 |
126 |
127 |
128 |
129 | ### General lattice attributes
130 |
131 |
132 | After configuring the lattice the attributes are available.
133 | Even without building a (finite) lattice structure all attributes can be computed on the fly for a given lattice vector,
134 | consisting of the translation vector `n` and the atom index `alpha`. For computing the (translated) atom positions
135 | the `get_position` method is used. Also, the neighbors and the vectors to these neighbors can be calculated.
136 | The `dist_idx`-parameter specifies the distance of the neighbors (0 for nearest neighbors, 1 for next nearest neighbors, ...):
137 | ````python
138 | from lattpy import simple_square
139 |
140 | latt = simple_square()
141 |
142 | # Get position of atom alpha=0 in the translated unit-cell
143 | positions = latt.get_position(n=[0, 0], alpha=0)
144 |
145 | # Get lattice-indices of the nearest neighbors of atom alpha=0 in the translated unit-cell
146 | neighbor_indices = latt.get_neighbors(n=[0, 0], alpha=0, distidx=0)
147 |
148 | # Get vectors to the nearest neighbors of atom alpha=0 in the translated unit-cell
149 | neighbor_vectors = latt.get_neighbor_vectors(alpha=0, distidx=0)
150 | ````
151 |
152 | Also, the reciprocal lattice vectors can be computed
153 | ````python
154 | rvecs = latt.reciprocal_vectors()
155 | ````
156 |
157 | or used to construct the reciprocal lattice:
158 | ````python
159 | rlatt = latt.reciprocal_lattice()
160 | ````
161 |
162 | The 1. Brillouin zone is the Wigner-Seitz cell of the reciprocal lattice:
163 | ````python
164 | bz = rlatt.wigner_seitz_cell()
165 | ````
166 |
167 | The 1.BZ can also be obtained by calling the explicit method of the direct lattice:
168 | ````python
169 | bz = latt.brillouin_zone()
170 | ````
171 |
172 |
173 | ### Finite lattice data
174 |
175 |
176 | If the lattice has been built the needed data is cached. The lattice sites of the
177 | structure then can be accessed by a simple index `i`. The syntax is the same as before,
178 | just without the `get_` prefix:
179 |
180 | ````python
181 | latt.build((5, 2))
182 | i = 2
183 |
184 | # Get position of the atom with index i=2
185 | positions = latt.position(i)
186 |
187 | # Get the atom indices of the nearest neighbors of the atom with index i=2
188 | neighbor_indices = latt.neighbors(i, distidx=0)
189 |
190 | # the nearest neighbors can also be found by calling (equivalent to dist_idx=0)
191 | neighbor_indices = latt.nearest_neighbors(i)
192 | ````
193 |
194 | ### Data map
195 |
196 | The lattice model makes it is easy to construct the (tight-binding) Hamiltonian of a non-interacting model:
197 |
198 |
199 | ````python
200 | import numpy as np
201 | from lattpy import simple_chain
202 |
203 | # Initializes a 1D lattice chain with a length of 5 atoms.
204 | latt = simple_chain(a=1.0)
205 | latt.build(shape=4)
206 | n = latt.num_sites
207 |
208 | # Construct the non-interacting (kinetic) Hamiltonian-matrix
209 | eps, t = 0., 1.
210 | ham = np.zeros((n, n))
211 | for i in range(n):
212 | ham[i, i] = eps
213 | for j in latt.nearest_neighbors(i):
214 | ham[i, j] = t
215 | ````
216 |
217 |
218 | Since we loop over all sites of the lattice the construction of the hamiltonian is slow.
219 | An alternative way of mapping the lattice data to the hamiltonian is using the `DataMap`
220 | object returned by the `map()` method of the lattice data. This stores the atom-types,
221 | neighbor-pairs and corresponding distances of the lattice sites. Using the built-in
222 | masks the construction of the hamiltonian-data can be vectorized:
223 | ````python
224 | from scipy import sparse
225 |
226 | # Vectorized construction of the hamiltonian
227 | eps, t = 0., 1.
228 | dmap = latt.data.map() # Build datamap
229 | values = np.zeros(dmap.size) # Initialize array for data of H
230 | values[dmap.onsite(alpha=0)] = eps # Map onsite-energies to array
231 | values[dmap.hopping(distidx=0)] = t # Map hopping-energies to array
232 |
233 | # The indices and data array can be used to construct a sparse matrix
234 | ham_s = sparse.csr_matrix((values, dmap.indices))
235 | ham = ham_s.toarray()
236 | ````
237 |
238 | Both construction methods will create the following Hamiltonian-matrix:
239 | ````
240 | [[0. 1. 0. 0. 0.]
241 | [1. 0. 1. 0. 0.]
242 | [0. 1. 0. 1. 0.]
243 | [0. 0. 1. 0. 1.]
244 | [0. 0. 0. 1. 0.]]
245 | ````
246 |
247 | ## 🔥 Performance
248 |
249 |
250 | Even though `lattpy` is written in pure python, it achieves high performance and
251 | a low memory footprint by making heavy use of numpy's vectorized operations and scipy's
252 | cKDTree. As an example the build times and memory usage in the build process for different
253 | lattices are shown in the following plots:
254 |
255 |
256 | | Build time | Build memory |
257 | |:-------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------:|
258 | |
|
|
259 |
260 |
261 |
262 | Note that the overhead of the multi-thread neighbor search results in a slight
263 | increase of the build time for small systems. By using `num_jobs=1` in the `build`-method
264 | this overhead can be eliminated for small systems. By passing `num_jobs=-1` all cores
265 | of the system are used.
266 |
267 |
268 | ## 💻 Development
269 |
270 | See the [CHANGELOG](https://github.com/dylanljones/lattpy/blob/master/CHANGELOG.md) for
271 | the recent changes of the project.
272 |
273 | If you encounter an issue or want to contribute to pyrekordbox, please feel free to
274 | get in touch, [create an issue](https://github.com/dylanljones/lattpy/issues/new/choose)
275 | or open a pull request! A guide for contributing to `lattpy` and the commit-message style can be found in
276 | [CONTRIBUTING](https://github.com/dylanljones/lattpy/blob/master/CONTRIBUTING.md)
277 |
278 |
279 | [docs-stable-link]: https://lattpy.readthedocs.io/en/stable/
280 | [docs-tutorial-link]: https://lattpy.readthedocs.io/en/stable/tutorial/index.html
281 |
282 | [tests-master]: https://img.shields.io/github/actions/workflow/status/dylanljones/lattpy/test.yml?branch=master&label=tests&logo=github&style=flat
283 | [tests-dev]: https://img.shields.io/github/actions/workflow/status/dylanljones/lattpy/test.yml?branch=dev&label=tests&logo=github&style=flat
284 | [link-tests]: https://github.com/dylanljones/lattpy/actions/workflows/test.yml
285 | [codecov-master]: https://codecov.io/gh/dylanljones/lattpy/branch/master/graph/badge.svg?
286 | [codecov-master-link]: https://app.codecov.io/gh/dylanljones/lattpy/branch/master
287 | [codecov-dev]: https://codecov.io/gh/dylanljones/lattpy/branch/dev/graph/badge.svg?
288 | [codecov-dev-link]: https://app.codecov.io/gh/dylanljones/lattpy/branch/dev
289 | [docs-master]: https://img.shields.io/readthedocs/lattpy/latest?style=flat&logo=readthedocs
290 | [docs-master-link]: https://lattpy.readthedocs.io/en/latest/
291 | [docs-dev]: https://img.shields.io/readthedocs/lattpy/dev?style=flat&logo=readthedocs
292 | [docs-dev-link]: https://lattpy.readthedocs.io/en/dev/
293 |
294 | [code-ql]: https://github.com/dylanljones/lattpy/actions/workflows/codeql.yml/badge.svg
295 | [code-ql-link]: https://github.com/dylanljones/lattpy/actions/workflows/codeql.yml
296 |
--------------------------------------------------------------------------------
/benchmarks/bench_build.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 |
7 | import os
8 | import time
9 | import tracemalloc
10 | import numpy as np
11 | import matplotlib.pyplot as plt
12 | import lattpy as lp
13 |
14 | MB = 1024 * 1024
15 | MAX_SITES = 10_000_000
16 | RUNS = 3
17 | MAX_POINTS = 30
18 |
19 | overwrite = False
20 | latts = {
21 | "chain": lp.simple_chain(),
22 | "square": lp.simple_square(),
23 | "hexagonal": lp.simple_hexagonal(),
24 | "cubic": lp.simple_cubic(),
25 | }
26 |
27 |
28 | class Profiler:
29 | """Profiler object for measuring time, memory and other stats."""
30 |
31 | def __init__(self):
32 | self._timefunc = time.perf_counter
33 | self._snapshot = None
34 | self._t0 = 0
35 | self._m0 = 0
36 | self._thread = None
37 |
38 | @property
39 | def seconds(self) -> float:
40 | """Returns the time since the timer has been started in seconds."""
41 | return self._timefunc() - self._t0
42 |
43 | @property
44 | def snapshot(self):
45 | return self._snapshot
46 |
47 | @property
48 | def memory(self):
49 | return self.get_memory() - self._m0
50 |
51 | def start(self):
52 | """Start the profiler."""
53 | tracemalloc.start()
54 | self._m0 = self.get_memory()
55 | self._t0 = self._timefunc()
56 |
57 | def stop(self) -> None:
58 | """Stop the profiler."""
59 | self._t0 = self._timefunc()
60 | tracemalloc.stop()
61 |
62 | def get_memory(self):
63 | self.take_snapshot()
64 | stats = self.statistics("filename")
65 | return sum(stat.size for stat in stats)
66 |
67 | def take_snapshot(self):
68 | snapshot = tracemalloc.take_snapshot()
69 | snapshot = snapshot.filter_traces(
70 | (
71 | tracemalloc.Filter(False, ""),
72 | tracemalloc.Filter(False, ""),
73 | )
74 | )
75 | self._snapshot = snapshot
76 | return snapshot
77 |
78 | def statistics(self, group="lineno"):
79 | return self._snapshot.statistics(group)
80 |
81 |
82 | def _benchmark_build_periodic(latt: lp.Lattice, sizes, runs, **kwargs):
83 | profiler = Profiler()
84 | data = np.zeros((len(sizes), 4))
85 | for i, size in enumerate(sizes):
86 | t, mem, per = 0.0, 0.0, 0.0
87 | for _ in range(runs):
88 | profiler.start()
89 | latt.build(np.ones(latt.dim) * size, **kwargs)
90 | t += profiler.seconds
91 | mem += profiler.memory
92 | profiler.start()
93 | latt.set_periodic(True)
94 | per += profiler.seconds
95 |
96 | data[i, 0] = latt.num_sites
97 | data[i, 1] = t / runs
98 | data[i, 2] = mem / runs
99 | data[i, 3] = per / runs
100 | return data
101 |
102 |
103 | def bench_build_periodic(latt, runs=RUNS):
104 | total_sizes = np.geomspace(10, MAX_SITES, MAX_POINTS).astype(np.int64)
105 | if latt.dim > 1:
106 | sizes = np.unique(np.power(total_sizes, 1 / latt.dim).astype(np.int64))
107 | else:
108 | sizes = total_sizes
109 | return _benchmark_build_periodic(latt, sizes, runs, primitive=True)
110 |
111 |
112 | def plot_benchmark_build(data):
113 | fig1, ax1 = plt.subplots()
114 | fig2, ax2 = plt.subplots()
115 | fig3, ax3 = plt.subplots()
116 |
117 | ax1.set_xscale("log")
118 | ax1.set_yscale("log")
119 | ax2.set_xscale("log")
120 | ax2.set_yscale("log")
121 | ax3.set_xscale("log")
122 | ax3.set_yscale("log")
123 |
124 | for name, arr in data.items():
125 | num_sites = arr[:, 0]
126 | line = ax1.plot(num_sites, arr[:, 1], label=name)[0]
127 | ax1.plot(num_sites, arr[:, 1], ls="--", color=line.get_color())
128 | ax2.plot(num_sites[1:], arr[1:, 2] / MB, label=name)
129 | ax3.plot(num_sites, arr[:, 3], label=name)
130 |
131 | ax1.legend()
132 | ax2.legend()
133 | ax3.legend()
134 |
135 | ax1.grid()
136 | ax2.grid()
137 | ax3.grid()
138 |
139 | ax1.set_ylabel("Time (s)")
140 | ax2.set_ylabel("Memory (MB)")
141 | ax3.set_ylabel("Time (s)")
142 | ax1.set_xlabel("N")
143 | ax2.set_xlabel("N")
144 | ax3.set_xlabel("N")
145 | ax1.set_ylim(0.001, 100)
146 | ax3.set_ylim(0.001, 100)
147 | fig1.tight_layout()
148 | fig2.tight_layout()
149 | fig3.tight_layout()
150 |
151 | fig1.savefig("bench_build_time.png")
152 | fig2.savefig("bench_build_memory.png")
153 | fig3.savefig("bench_periodic_time.png")
154 |
155 |
156 | def main():
157 | file = "benchmark_build_periodic.npz"
158 | if overwrite or not os.path.exists(file):
159 | data = dict()
160 | for name, latt in latts.items():
161 | print("Benchmarking build:", name)
162 | values = bench_build_periodic(latt)
163 | data[name] = values
164 | np.savez(file, **data)
165 | else:
166 | data = np.load(file)
167 |
168 | plot_benchmark_build(data)
169 | plt.show()
170 |
171 |
172 | if __name__ == "__main__":
173 | main()
174 |
--------------------------------------------------------------------------------
/benchmarks/bench_build_memory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dylanljones/lattpy/852005e4ec85118229c1237cd1eb6272053a0f5f/benchmarks/bench_build_memory.png
--------------------------------------------------------------------------------
/benchmarks/bench_build_time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dylanljones/lattpy/852005e4ec85118229c1237cd1eb6272053a0f5f/benchmarks/bench_build_time.png
--------------------------------------------------------------------------------
/benchmarks/bench_periodic_time.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dylanljones/lattpy/852005e4ec85118229c1237cd1eb6272053a0f5f/benchmarks/bench_periodic_time.png
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 |
15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16 |
17 | help:
18 | @echo "Please use \`make ' where is one of"
19 | @echo " html to make standalone HTML files"
20 | @echo " dirhtml to make HTML files named index.html in directories"
21 | @echo " singlehtml to make a single large HTML file"
22 | @echo " pickle to make pickle files"
23 | @echo " json to make JSON files"
24 | @echo " htmlhelp to make HTML files and a HTML help project"
25 | @echo " qthelp to make HTML files and a qthelp project"
26 | @echo " devhelp to make HTML files and a Devhelp project"
27 | @echo " epub to make an epub"
28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 | @echo " text to make text files"
31 | @echo " man to make manual pages"
32 | @echo " changes to make an overview of all changed/added/deprecated items"
33 | @echo " linkcheck to check all external links for integrity"
34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
35 |
36 | clean:
37 | -rm -rf $(BUILDDIR)/*
38 |
39 | html:
40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
41 | @echo
42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
43 |
44 | dirhtml:
45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
48 |
49 | singlehtml:
50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
51 | @echo
52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
53 |
54 | pickle:
55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
56 | @echo
57 | @echo "Build finished; now you can process the pickle files."
58 |
59 | json:
60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
61 | @echo
62 | @echo "Build finished; now you can process the JSON files."
63 |
64 | htmlhelp:
65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
66 | @echo
67 | @echo "Build finished; now you can run HTML Help Workshop with the" \
68 | ".hhp project file in $(BUILDDIR)/htmlhelp."
69 |
70 | qthelp:
71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
72 | @echo
73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Mapnik.qhcp"
76 | @echo "To view the help file:"
77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Mapnik.qhc"
78 |
79 | devhelp:
80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
81 | @echo
82 | @echo "Build finished."
83 | @echo "To view the help file:"
84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Mapnik"
85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Mapnik"
86 | @echo "# devhelp"
87 |
88 | epub:
89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
90 | @echo
91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
92 |
93 | latex:
94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 | @echo
96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
98 | "(use \`make latexpdf' here to do that automatically)."
99 |
100 | latexpdf:
101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
102 | @echo "Running LaTeX files through pdflatex..."
103 | make -C $(BUILDDIR)/latex all-pdf
104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
105 |
106 | text:
107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
108 | @echo
109 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
110 |
111 | man:
112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
113 | @echo
114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
115 |
116 | changes:
117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
118 | @echo
119 | @echo "The overview file is in $(BUILDDIR)/changes."
120 |
121 | linkcheck:
122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
123 | @echo
124 | @echo "Link check complete; look for any errors in the above output " \
125 | "or in $(BUILDDIR)/linkcheck/output.txt."
126 |
127 | doctest:
128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
129 | @echo "Testing of doctests in the sources finished, look at the " \
130 | "results in $(BUILDDIR)/doctest/output.txt."
131 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.https://www.sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/.gitignore:
--------------------------------------------------------------------------------
1 | generated
2 |
--------------------------------------------------------------------------------
/docs/source/_static/example_square_periodic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dylanljones/lattpy/852005e4ec85118229c1237cd1eb6272053a0f5f/docs/source/_static/example_square_periodic.png
--------------------------------------------------------------------------------
/docs/source/_static/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dylanljones/lattpy/852005e4ec85118229c1237cd1eb6272053a0f5f/docs/source/_static/header.png
--------------------------------------------------------------------------------
/docs/source/_templates/apidoc/module.rst_t:
--------------------------------------------------------------------------------
1 | {%- if show_headings %}
2 | {{- [basename] | join(' ') | e | heading }}
3 | {% endif -%}
4 |
5 | .. automodule:: {{ qualname }}
6 | {%- for option in automodule_options %}
7 | :{{ option }}:
8 | {%- endfor %}
9 |
--------------------------------------------------------------------------------
/docs/source/_templates/apidoc/package.rst_t:
--------------------------------------------------------------------------------
1 | {%- macro automodule(modname, options) -%}
2 | .. automodule:: {{ modname }}
3 | {%- for option in options %}
4 | :{{ option }}:
5 | {%- endfor %}
6 | {%- endmacro %}
7 |
8 | {%- macro toctree(docnames) -%}
9 | .. toctree::
10 | :maxdepth: {{ maxdepth }}
11 | {% for docname in docnames %}
12 | {{ docname }}
13 | {%- endfor %}
14 | {%- endmacro %}
15 |
16 | {%- if is_namespace %}
17 | {{- [pkgname, "namespace"] | join(" ") | e | heading }}
18 | {% else %}
19 | {{- [pkgname, "package"] | join(" ") | e | heading }}
20 | {% endif %}
21 |
22 | {%- if is_namespace %}
23 | .. py:module:: {{ pkgname }}
24 | {% endif %}
25 |
26 | {%- if modulefirst and not is_namespace %}
27 | {{ automodule(pkgname, automodule_options) }}
28 | {% endif %}
29 |
30 | {%- if subpackages %}
31 | Subpackages
32 | -----------
33 |
34 | {{ toctree(subpackages) }}
35 | {% endif %}
36 |
37 | {%- if submodules %}
38 | Submodules
39 | ----------
40 | {% if separatemodules %}
41 | {{ toctree(submodules) }}
42 | {% else %}
43 | {%- for submodule in submodules %}
44 | {% if show_headings %}
45 | {{- [submodule, "module"] | join(" ") | e | heading(2) }}
46 | {% endif %}
47 | {{ automodule(submodule, automodule_options) }}
48 | {% endfor %}
49 | {%- endif %}
50 | {%- endif %}
51 |
52 | {%- if not modulefirst and not is_namespace %}
53 | Module contents
54 | ---------------
55 |
56 | {{ automodule(pkgname, automodule_options) }}
57 | {% endif %}
58 |
--------------------------------------------------------------------------------
/docs/source/_templates/apidoc/toc.rst_t:
--------------------------------------------------------------------------------
1 | {{ header | heading }}
2 |
3 | .. toctree::
4 | :maxdepth: {{ maxdepth }}
5 | {% for docname in docnames %}
6 | {{ docname }}
7 | {%- endfor %}
8 |
--------------------------------------------------------------------------------
/docs/source/_templates/autosummary/class.rst:
--------------------------------------------------------------------------------
1 | {{ fullname | escape | underline}}
2 |
3 | .. currentmodule:: {{ module }}
4 |
5 | .. autoclass:: {{ objname }}
6 |
7 | {% block methods %}
8 | .. automethod:: __init__
9 | :noindex:
10 |
11 | {% if methods %}
12 | .. rubric:: Methods
13 |
14 | .. autosummary::
15 | :toctree:
16 | {% for item in methods %}
17 | ~{{ name }}.{{ item }}
18 | {%- endfor %}
19 | {% endif %}
20 | {% endblock %}
21 |
22 | {% block attributes %}
23 | {% if attributes %}
24 | .. rubric:: Attributes
25 |
26 | .. autosummary::
27 | :toctree:
28 | {% for item in attributes %}
29 | ~{{ name }}.{{ item }}
30 | {%- endfor %}
31 | {% endif %}
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/docs/source/_templates/autosummary/module.rst:
--------------------------------------------------------------------------------
1 | {{ fullname }}
2 | {{ underline }}
3 |
4 | .. module:: {{fullname}}
5 |
6 | .. automodule:: {{ fullname }}
7 | :noindex:
8 |
9 | API
10 | ---
11 |
12 | .. {% if classes %}
13 | .. .. inheritance-diagram:: {{ fullname }}
14 | .. :parts: 1
15 |
16 | {% endif %}
17 |
18 | {% block functions %}
19 | {% if functions or methods %}
20 | .. rubric:: Functions
21 |
22 | .. autosummary::
23 | :toctree:
24 | {% for item in functions %}
25 | {{ item }}
26 | {%- endfor %}
27 | {% for item in methods %}
28 | {{ item }}
29 | {%- endfor %}
30 | {% endif %}
31 | {% endblock %}
32 |
33 | {% block classes %}
34 | {% if classes %}
35 | .. rubric:: Classes
36 |
37 | .. autosummary::
38 | :toctree:
39 | {% for item in classes %}
40 | {{ item }}
41 | {%- endfor %}
42 | {% endif %}
43 | {% endblock %}
44 |
45 | {% block exceptions %}
46 | {% if exceptions %}
47 | .. rubric:: Exceptions
48 |
49 | .. autosummary::
50 | :toctree:
51 | {% for item in classes %}
52 | {{ item }}
53 | {%- endfor %}
54 | {% endif %}
55 | {% endblock %}
56 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 |
13 | import os
14 | import sys
15 | import math
16 |
17 | sys.path.insert(0, os.path.abspath("../.."))
18 |
19 | import lattpy
20 |
21 |
22 | # -- Project information -----------------------------------------------------
23 |
24 | project = "lattpy"
25 | copyright = "2022, Dylan L. Jones"
26 | author = "Dylan L. Jones"
27 |
28 | # -- General configuration ---------------------------------------------------
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = [
34 | "sphinx.ext.autodoc",
35 | "numpydoc",
36 | "myst_parser",
37 | "sphinx.ext.autosummary",
38 | "matplotlib.sphinxext.plot_directive",
39 | "sphinx.ext.intersphinx", # links to numpy, scipy ... docs
40 | "sphinx.ext.viewcode",
41 | "sphinx.ext.doctest",
42 | "sphinx.ext.coverage",
43 | "sphinx.ext.extlinks", # define roles for links
44 | "sphinx_toggleprompt", # toggle `>>>`
45 | "sphinx_rtd_theme",
46 | "sphinx.ext.graphviz",
47 | "sphinx.ext.inheritance_diagram",
48 | ]
49 |
50 | # Add any paths that contain templates here, relative to this directory.
51 | templates_path = ["_templates"]
52 |
53 | # List of patterns, relative to source directory, that match files and
54 | # directories to ignore when looking for source files.
55 | # This pattern also affects html_static_path and html_extra_path.
56 |
57 | exclude_patterns = [
58 | "_build",
59 | "Thumbs.db",
60 | ".DS_Store",
61 | "tests",
62 | "generated/lattpy.rst",
63 | "generated/modules.rst",
64 | ]
65 |
66 | # -- Graphviz options --------------------------------------------------------
67 |
68 | graphviz_output_format = "svg"
69 | inheritance_graph_attrs = dict(rankdir="LR", size='""')
70 |
71 |
72 | # -- Options for HTML output -------------------------------------------------
73 |
74 | # The theme to use for HTML and HTML Help pages. See the documentation for
75 | # a list of builtin themes.
76 |
77 | html_theme = "sphinx_rtd_theme"
78 |
79 | # Add any paths that contain custom static files (such as style sheets) here,
80 | # relative to this directory. They are copied after the builtin static files,
81 | # so a file named "default.css" will overwrite the builtin "default.css".
82 | html_static_path = ["_static"]
83 |
84 | # Include Markdown parser
85 | # source_parsers = {
86 | # '.md': 'recommonmark.parser.CommonMarkParser',
87 | # }
88 | source_suffix = [".rst", ".md"]
89 |
90 | # Don't show type hints
91 | autodoc_typehints = "none"
92 |
93 | # Preserve order
94 | autodoc_member_order = "bysource"
95 |
96 | # -- Apidoc ------------------------------------------------------------------
97 |
98 | add_module_names = True
99 |
100 |
101 | # -- Autosummary -------------------------------------------------------------
102 |
103 | autosummary_generate = True
104 | # autosummary_imported_members = True
105 |
106 | # -- Numpy extension ---------------------------------------------------------
107 |
108 | numpydoc_use_plots = True
109 | # numpydoc_xref_param_type = True
110 | # numpydoc_xref_ignore = "all" # not working...
111 | numpydoc_show_class_members = False
112 |
113 | # -- Plots -------------------------------------------------------------------
114 |
115 | plot_pre_code = """
116 | import warnings
117 | import numpy as np
118 | import matplotlib.pyplot as plt
119 | import lattpy as lp
120 | np.random.seed(0)
121 | """
122 |
123 | doctest_global_setup = plot_pre_code # make doctests consistent
124 | doctest_global_cleanup = """
125 | try:
126 | plt.close() # close any open figures
127 | except:
128 | pass
129 | """
130 |
131 | plot_include_source = True
132 | plot_html_show_source_link = False
133 | plot_formats = [("png", 100), "pdf"]
134 |
135 | phi = (math.sqrt(5) + 1) / 2
136 |
137 | plot_rcparams = {
138 | "font.size": 8,
139 | "axes.titlesize": 8,
140 | "axes.labelsize": 8,
141 | "xtick.labelsize": 8,
142 | "ytick.labelsize": 8,
143 | "legend.fontsize": 8,
144 | "figure.figsize": (3 * phi, 3),
145 | "figure.subplot.bottom": 0.2,
146 | "figure.subplot.left": 0.2,
147 | "figure.subplot.right": 0.9,
148 | "figure.subplot.top": 0.85,
149 | "figure.subplot.wspace": 0.4,
150 | "text.usetex": False,
151 | }
152 |
153 | # -- Intersphinx -------------------------------------------------------------
154 |
155 | # taken from https://gist.github.com/bskinn/0e164963428d4b51017cebdb6cda5209
156 | intersphinx_mapping = {
157 | "python": (r"https://docs.python.org", None),
158 | "scipy": (r"https://docs.scipy.org/doc/scipy/", None),
159 | "numpy": (r"https://docs.scipy.org/doc/numpy/", None),
160 | "np": (r"https://docs.scipy.org/doc/numpy/", None),
161 | "matplotlib": (r"https://matplotlib.org/", None),
162 | }
163 |
164 |
165 | # -- Auto-run sphinx-apidoc --------------------------------------------------
166 |
167 |
168 | def run_apidoc(_):
169 | from sphinx.ext.apidoc import main
170 | import os
171 | import sys
172 |
173 | sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
174 | cur_dir = os.path.abspath(os.path.dirname(__file__))
175 | proj_dir = os.path.dirname(os.path.dirname(cur_dir))
176 | doc_dir = os.path.join(proj_dir, "docs")
177 | output_path = os.path.join(doc_dir, "source", "generated")
178 | module = os.path.join(proj_dir, "lattpy")
179 | exclude = os.path.join(module, "tests")
180 | template_dir = os.path.join(doc_dir, "source", "_templates", "apidoc")
181 | main(["-fMeT", "-o", output_path, module, exclude, "--templatedir", template_dir])
182 |
183 |
184 | def setup(app):
185 | app.connect("builder-inited", run_apidoc)
186 |
--------------------------------------------------------------------------------
/docs/source/development/changes.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../../CHANGELOG.md
2 |
3 | ```
4 |
--------------------------------------------------------------------------------
/docs/source/development/contributing.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../../CONTRIBUTING.md
2 |
3 | ```
4 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. lattpy documentation master file, created by
2 | sphinx-quickstart on Mon Jan 31 12:11:09 2022.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. image:: _static/header.png
7 | :align: center
8 | :alt: Graphene lattice
9 |
10 | ===============================================
11 | Simple and Efficient Lattice Modeling in Python
12 | ===============================================
13 |
14 | |pypi-python-version| |pypi-version| |pypi-status| |pypi-license| |lgtm-grade| |style-black|
15 |
16 | “Any dimension and shape you like.”
17 |
18 | *LattPy* is a simple and efficient Python package for modeling Bravais lattices and
19 | constructing (finite) lattice structures in any dimension.
20 | It provides an easy interface for constructing lattice structures by simplifying the
21 | configuration of the unit cell and the neighbor connections - making it possible to
22 | construct complex models in just a few lines of code and without the headache of
23 | adding neighbor connections manually. You will save time and mental energy for more important matters.
24 |
25 |
26 | +---------+---------------+-----------------+---------------+
27 | | Master ||tests-master| ||codecov-master| | |docs-master| |
28 | +---------+---------------+-----------------+---------------+
29 | | Dev ||tests-dev| ||codecov-dev| | |docs-dev| |
30 | +---------+---------------+-----------------+---------------+
31 |
32 | .. warning::
33 | This project is still in development and might change significantly in the future!
34 |
35 | .. toctree::
36 | :maxdepth: 3
37 | :caption: User Guide
38 |
39 | installation
40 | quickstart
41 | tutorial/index
42 |
43 |
44 | .. toctree::
45 | :maxdepth: 1
46 | :titlesonly:
47 | :caption: API Reference
48 |
49 | lattpy
50 | generated/lattpy.atom
51 | generated/lattpy.basis
52 | generated/lattpy.data
53 | generated/lattpy.disptools
54 | generated/lattpy.lattice
55 | generated/lattpy.plotting
56 | generated/lattpy.shape
57 | generated/lattpy.spatial
58 | generated/lattpy.structure
59 | generated/lattpy.utils
60 |
61 |
62 | .. toctree::
63 | :maxdepth: 3
64 | :caption: Development
65 |
66 | development/changes
67 | development/contributing
68 |
69 | Indices and tables
70 | ==================
71 |
72 | * :ref:`genindex`
73 | * :ref:`modindex`
74 | * :ref:`search`
75 |
76 |
77 | .. |pypi-python-version| image:: https://img.shields.io/pypi/pyversions/lattpy?logo=python&style=flat-square
78 | :alt: PyPI - Python Version
79 | :target: https://pypi.org/project/lattpy/
80 | .. |pypi-version| image:: https://img.shields.io/pypi/v/lattpy?logo=pypi&style=flat-square
81 | :alt: PyPI - Version
82 | :target: https://pypi.org/project/lattpy/
83 | .. |pypi-status| image:: https://img.shields.io/pypi/status/lattpy?color=yellow&style=flat-square
84 | :alt: PyPI - Status
85 | :target: https://pypi.org/project/lattpy/
86 | .. |pypi-license| image:: https://img.shields.io/pypi/l/lattpy?style=flat-square
87 | :alt: PyPI - License
88 | :target: https://github.com/dylanljones/lattpy/blob/master/LICENSE
89 | .. |lgtm-grade| image:: https://img.shields.io/lgtm/grade/python/github/dylanljones/lattpy?label=code%20quality&logo=lgtm&style=flat-square
90 | :alt: LGTM Grade
91 | :target: https://lgtm.com/projects/g/dylanljones/lattpy/context:python
92 | .. |style-black| image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square
93 | :alt: Code style: black
94 | :target: https://github.com/psf/black
95 |
96 | .. |tests-master| image:: https://img.shields.io/github/workflow/status/dylanljones/lattpy/Test/master?label=tests&logo=github&style=flat
97 | :alt: Test status master
98 | :target: https://github.com/dylanljones/lattpy/actions/workflows/test.yml
99 | .. |tests-dev| image:: https://img.shields.io/github/workflow/status/dylanljones/lattpy/Test/dev?label=tests&logo=github&style=flat
100 | :alt: Test status dev
101 | :target: https://github.com/dylanljones/lattpy/actions/workflows/test.yml
102 | .. |codecov-master| image:: https://codecov.io/gh/dylanljones/lattpy/branch/master/graph/badge.svg?token=P61R3IQKXC
103 | :alt: Coverage master
104 | :target: https://app.codecov.io/gh/dylanljones/lattpy/branch/master
105 | .. |codecov-dev| image:: https://codecov.io/gh/dylanljones/lattpy/branch/dev/graph/badge.svg?token=P61R3IQKXC
106 | :alt: Coverage dev
107 | :target: https://app.codecov.io/gh/dylanljones/lattpy/branch/dev
108 | .. |docs-master| image:: https://img.shields.io/readthedocs/lattpy/latest?style=flat
109 | :alt: Docs master
110 | :target: https://lattpy.readthedocs.io/en/latest/
111 | .. |docs-dev| image:: https://img.shields.io/readthedocs/lattpy/dev?style=flat
112 | :alt: Docs dev
113 | :target: https://lattpy.readthedocs.io/en/dev/
114 |
--------------------------------------------------------------------------------
/docs/source/installation.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 | LattPy is available on PyPI_:
4 |
5 | .. code-block:: console
6 |
7 | $ pip install lattpy
8 |
9 | Alternatively, it can be installed via GitHub_:
10 |
11 | .. code-block:: console
12 |
13 | $ pip install git+https://github.com/dylanljones/lattpy.git@VERSION
14 |
15 | where `VERSION` is a release or tag (e.g. `0.6.4`). The project can also be
16 | cloned/forked and installed via
17 |
18 | .. code-block:: console
19 |
20 | $ python setup.py install
21 |
22 |
23 | .. _PyPi:
24 | https://pypi.org/project/lattpy/
25 | .. _GitHub:
26 | https://github.com/dylanljones/lattpy
27 |
--------------------------------------------------------------------------------
/docs/source/lattpy.rst:
--------------------------------------------------------------------------------
1 | lattpy
2 | ======
3 |
4 | .. automodule:: lattpy
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/source/quickstart.rst:
--------------------------------------------------------------------------------
1 | Quick-Start
2 | ===========
3 |
4 | A new instance of a lattice model is initialized using the unit-vectors of the Bravais lattice.
5 | After the initialization the atoms of the unit-cell need to be added. To finish the configuration
6 | the connections between the atoms in the lattice have to be set. This can either be done for
7 | each atom-pair individually by calling ``add_connection`` or for all possible pairs at once by
8 | callling ``add_connections``. The argument is the number of unique
9 | distances of neighbors. Setting a value of ``1`` will compute only the nearest
10 | neighbors of the atom.
11 |
12 | >>> import numpy as np
13 | >>> from lattpy import Lattice
14 | >>>
15 | >>> latt = Lattice(np.eye(2)) # Construct a Bravais lattice with square unit-vectors
16 | >>> latt.add_atom(pos=[0.0, 0.0]) # Add an Atom to the unit cell of the lattice
17 | >>> latt.add_connections(1) # Set the maximum number of distances between all atoms
18 | >>>
19 | >>> latt = Lattice(np.eye(2)) # Construct a Bravais lattice with square unit-vectors
20 | >>> latt.add_atom(pos=[0.0, 0.0], atom="A") # Add an Atom to the unit cell of the lattice
21 | >>> latt.add_atom(pos=[0.5, 0.5], atom="B") # Add an Atom to the unit cell of the lattice
22 | >>> latt.add_connection("A", "A", 1) # Set the max number of distances between A and A
23 | >>> latt.add_connection("A", "B", 1) # Set the max number of distances between A and B
24 | >>> latt.add_connection("B", "B", 1) # Set the max number of distances between B and B
25 | >>> latt.analyze()
26 |
27 |
28 | Configuring all connections using the ``add_connections``-method will call the ``analyze``-method
29 | directly. Otherwise this has to be called at the end of the lattice setup or by using
30 | ``analyze=True`` in the last call of ``add_connection``. This will compute the number of neighbors,
31 | their distances and their positions for each atom in the unitcell.
32 |
33 | To speed up the configuration prefabs of common lattices are included. The previous lattice
34 | can also be created with
35 |
36 | >>> from lattpy import simple_square
37 | >>> latt = simple_square(a=1.0, neighbors=1)
38 |
39 | .. autosummary::
40 |
41 | lattpy.simple_chain
42 | lattpy.alternating_chain
43 | lattpy.simple_square
44 | lattpy.simple_rectangular
45 | lattpy.graphene
46 | lattpy.simple_cubic
47 | lattpy.nacl_structure
48 |
49 |
50 | So far only the lattice structure has been configured. To actually construct a (finite) model of the lattice
51 | the model has to be built:
52 |
53 | >>> latt.build(shape=(5, 3))
54 |
55 |
56 | To view the built lattice the ``plot``-method can be used:
57 |
58 | .. plot::
59 | :format: doctest
60 | :include-source:
61 | :context: close-figs
62 |
63 | >>> import matplotlib.pyplot as plt
64 | >>> from lattpy import simple_square
65 | >>> latt = simple_square()
66 | >>> latt.build(shape=(5, 3))
67 | >>> latt.plot()
68 | >>> plt.show()
69 |
70 |
71 | After configuring the lattice the attributes are available. Even without building
72 | a (finite) lattice structure all attributes can be computed on the fly for a given
73 | lattice vector, consisting of the translation vector ``n`` and the atom index ``alpha``.
74 | For computing the (translated) atom positions the ``get_position`` method is used.
75 | Also, the neighbors and the vectors to these neighbors can be calculated.
76 | The ``dist_idx``-parameter specifies the distance of the neighbors
77 | (0 for nearest neighbors, 1 for next nearest neighbors, ...):
78 |
79 | >>> latt.get_position(n=[0, 0], alpha=0)
80 | [0. 0.]
81 | >>> latt.get_neighbors([0, 0], alpha=0, distidx=0)
82 | [[ 1 0 0]
83 | [ 0 -1 0]
84 | [-1 0 0]
85 | [ 0 1 0]]
86 | >>> latt.get_neighbor_vectors(alpha=0, distidx=0)
87 | [[ 1. 0.]
88 | [ 0. -1.]
89 | [-1. 0.]
90 | [ 0. 1.]]
91 |
92 |
93 | Also, the reciprocal lattice vectors can be computed
94 |
95 | >>> latt.reciprocal_vectors()
96 | [[6.28318531 0. ]
97 | [0. 6.28318531]]
98 |
99 | or used to construct the reciprocal lattice:
100 |
101 | >>> rlatt = latt.reciprocal_lattice()
102 |
103 | The 1. Brillouin zone is the Wigner-Seitz cell of the reciprocal lattice:
104 |
105 | >>> bz = rlatt.wigner_seitz_cell()
106 |
107 | The 1.BZ can also be obtained by calling the explicit method of the direct lattice:
108 |
109 | >>> bz = latt.brillouin_zone()
110 |
111 |
112 | If the lattice has been built the necessary data is cached. The lattice sites of the
113 | structure then can be accessed by a simple index ``i``. The syntax is the same as before,
114 | just without the ``get_`` prefix:
115 |
116 | >>> i = 2
117 | >>>
118 | >>> # Get position of the atom with index i=2
119 | >>> positions = latt.position(i)
120 | >>> # Get the atom indices of the nearest neighbors of the atom with index i=2
121 | >>> neighbor_indices = latt.neighbors(i, distidx=0)
122 | >>> # the nearest neighbors can also be found by calling (equivalent to dist_idx=0)
123 | >>> neighbor_indices = latt.nearest_neighbors(i)
124 |
--------------------------------------------------------------------------------
/docs/source/requirements.txt:
--------------------------------------------------------------------------------
1 | numpydoc
2 | matplotlib
3 | sphinx>3.0.0 # https://github.com/readthedocs/readthedocs.org/issues/8616
4 | sphinx_toggleprompt
5 | sphinx_rtd_theme
6 | myst-parser
7 | graphviz
8 |
--------------------------------------------------------------------------------
/docs/source/tutorial/configuration.rst:
--------------------------------------------------------------------------------
1 |
2 | Configuration
3 | -------------
4 |
5 | The ``Lattice`` object of LattPy can be configured in a few steps. There are three
6 | fundamental steps to defining a new structure:
7 |
8 | 1. Defining basis vectors of the lattice
9 | 2. Adding atoms to the unit cell
10 | 3. Adding connections to neighbors
11 |
12 |
13 | Basis vectors
14 | ~~~~~~~~~~~~~
15 |
16 | The core of a Bravais lattice are the basis vectors :math:`\boldsymbol{A} = \boldsymbol{a}_i`
17 | with :math:`i=1, \dots, d`, which define the unit cell of the lattice.
18 | Each lattice point is defined by a translation vector :math:`\boldsymbol{n} = (n_1, \dots, n_d)`:
19 |
20 | .. math::
21 | \boldsymbol{R_n} = \sum_{i=1}^d n_i \boldsymbol{a}_i.
22 |
23 | A new ``Lattice`` instance can be created by simply passing the basis vectors of the
24 | system. A one-dimensional lattice can be initialized by passing a scalar or an
25 | :math:`1 \times 1` array to the ``Lattice`` constructor:
26 |
27 | >>> latt = lp.Lattice(1.0)
28 | >>> latt.vectors
29 | [[1.0]]
30 |
31 | For higher dimensional lattices an :math:`d \times d` array with the basis vectors
32 | as rows,
33 |
34 | .. math::
35 | \boldsymbol{A} = \begin{pmatrix}
36 | a_{11} & \dots & a_{1d} \\
37 | \vdots & \ddots & \vdots \\
38 | a_{d1} & \dots & a_{dd}
39 | \end{pmatrix},
40 |
41 | is expected. A square
42 | lattice, for example, can be initialized by a 2D identity matrix:
43 |
44 | >>> latt = lp.Lattice(np.eye(2))
45 | >>> latt.vectors
46 | [[1. 0.]
47 | [0. 1.]]
48 |
49 |
50 | The basis vectors of frequently used lattices can be intialized via the class-methods of the
51 | ``Lattice`` object, for example:
52 |
53 | .. autosummary::
54 |
55 | lattpy.Lattice.chain
56 | lattpy.Lattice.square
57 | lattpy.Lattice.rectangular
58 | lattpy.Lattice.hexagonal
59 | lattpy.Lattice.oblique
60 | lattpy.Lattice.hexagonal3d
61 | lattpy.Lattice.sc
62 | lattpy.Lattice.fcc
63 | lattpy.Lattice.bcc
64 |
65 |
66 | The resulting unit cell of the lattice can be visualized via the
67 | :py:func:`plot_cell() ` method:
68 |
69 | .. plot::
70 | :format: doctest
71 | :include-source:
72 | :context: close-figs
73 |
74 | >>> latt = lp.Lattice.hexagonal(a=1)
75 | >>> latt.plot_cell()
76 | >>> plt.show()
77 |
78 | Adding atoms
79 | ~~~~~~~~~~~~
80 |
81 | Until now only the lattice type has been defined via the basis vectors.
82 | To define a lattice structure we also have to specify the basis of the lattice
83 | by adding atoms to the unit cell. The positions of the atoms in the lattice
84 | then are given by
85 |
86 | .. math::
87 | \boldsymbol{R}_{n\alpha} = \boldsymbol{R_n} + \boldsymbol{r_\alpha},
88 |
89 | where :math:`\boldsymbol{r_\mu}` is the position of the atom :math:`\alpha` relative to
90 | the origin of the unit cell.
91 |
92 | In LattPy, atoms can be added to the ``Lattice`` object by calling :py:func:`add_atom() `
93 | and supplying the position and type of the atom:
94 |
95 | >>> latt = lp.Lattice.square()
96 | >>> latt.add_atom([0.0, 0.0], "A")
97 |
98 | If the position is omitted the atom is placed at the origin of the unit cell.
99 | The type of the atom can either be the name or an ``Atom`` instance:
100 |
101 | >>> latt = lp.Lattice.square()
102 | >>> latt.add_atom([0.0, 0.0], "A")
103 | >>> latt.add_atom([0.5, 0.5], lp.Atom("B"))
104 | >>> latt.atoms[0]
105 | Atom(A, size=10, 0)
106 | >>> latt.atoms[1]
107 | Atom(B, size=10, 1)
108 |
109 | If a name is passed, a new ``Atom`` instance is created.
110 | We again can view the current state of the unit cell:
111 |
112 | .. plot::
113 | :format: doctest
114 | :include-source:
115 | :context: close-figs
116 |
117 | >>> latt = lp.Lattice.square()
118 | >>> latt.add_atom([0.0, 0.0], "A")
119 | >>> ax = latt.plot_cell()
120 | >>> ax.set_xlim(-0.3, 1.3)
121 | >>> ax.set_ylim(-0.3, 1.3)
122 | >>> plt.show()
123 |
124 |
125 | Adding connections
126 | ~~~~~~~~~~~~~~~~~~
127 |
128 | Finally, the connections of the atoms to theirs neighbors have to be set up. LattPy
129 | automatically connects the neighbors of sites up to a specified level of neighbor
130 | distances, i.e. nearest neighbors, next nearest neighbors and so on. The maximal
131 | neighbor distance can be configured for each pair of atoms independently.
132 | Assuming a square lattice with two atoms A and B in the unit cell, the connections
133 | between the A atoms can be set to next nearest neighbors, while the connections
134 | between A and B can be set to nearest neighbors only:
135 |
136 | >>> latt = lp.Lattice.square()
137 | >>> latt.add_atom([0.0, 0.0], "A")
138 | >>> latt.add_atom([0.5, 0.5], "B")
139 | >>> latt.add_connection("A", "A", 2)
140 | >>> latt.add_connection("A", "B", 1)
141 | >>> latt.analyze()
142 |
143 | After setting up all the desired connections in the lattice the ``analyze`` method
144 | has to be called. This computes the actual neighbors for all configured distances
145 | of the atoms in the unit cell. Alternatively, the distances for all pairs of the sites in the unit cell can be
146 | configured at once by calling the ``add_connections`` method, which internally
147 | calls the ``analyze`` method. This speeds up the configuration of simple lattices.
148 |
149 | The final unit cell of the lattice, including the atoms and the neighbor information,
150 | can again be visualized:
151 |
152 | .. plot::
153 | :format: doctest
154 | :include-source:
155 | :context: close-figs
156 |
157 | >>> latt = lp.Lattice.square()
158 | >>> latt.add_atom()
159 | >>> latt.add_connections(1)
160 | >>> latt.plot_cell()
161 | >>> plt.show()
162 |
--------------------------------------------------------------------------------
/docs/source/tutorial/finite.rst:
--------------------------------------------------------------------------------
1 | Finite lattice models
2 | ---------------------
3 |
4 | So far only abstract, infinite lattices have been discussed. In order to construct
5 | a finite sized model of the configured lattice structure we have to build the lattice.
6 |
7 | Build geometries
8 | ~~~~~~~~~~~~~~~~
9 |
10 | By default, the shape passed to the ``build`` is used to create a box in cartesian
11 | coordinates. Alternatively, the geometry can be constructed in the basis of the lattice
12 | by setting ``primitive=True``. As an example, consider the hexagonal lattice. We can
13 | build the lattice in a box of the specified shape:
14 |
15 | .. plot::
16 | :format: doctest
17 | :include-source:
18 | :context: close-figs
19 |
20 | >>> latt = lp.Lattice.hexagonal()
21 | >>> latt.add_atom()
22 | >>> latt.add_connections()
23 | >>> s = latt.build((10, 10))
24 | >>> ax = latt.plot()
25 | >>> s.plot(ax)
26 | >>> plt.show()
27 |
28 |
29 | or in the coordinate system of the lattice, which results in
30 |
31 |
32 | .. plot::
33 | :format: doctest
34 | :include-source:
35 | :context: close-figs
36 |
37 | >>> latt = lp.Lattice.hexagonal()
38 | >>> latt.add_atom()
39 | >>> latt.add_connections()
40 | >>> s = latt.build((10, 10), primitive=True)
41 | >>> ax = latt.plot()
42 | >>> s.plot(ax)
43 | >>> plt.show()
44 |
45 |
46 |
47 | Other geometries can be build by using ``AbstractShape`` ojects:
48 |
49 | .. plot::
50 | :format: doctest
51 | :include-source:
52 | :context: close-figs
53 |
54 | >>> latt = lp.Lattice.hexagonal()
55 | >>> latt.add_atom()
56 | >>> latt.add_connections()
57 | >>> s = lp.Circle((0, 0), radius=10)
58 | >>> latt.build(s, primitive=True)
59 | >>> ax = latt.plot()
60 | >>> s.plot(ax)
61 | >>> plt.show()
62 |
63 |
64 | Periodic boundary conditions
65 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
66 |
67 | After a finite size lattice model has been buildt periodic boundary conditions can
68 | be configured by specifying the axis of the periodic boundary conditions.
69 | The periodic boundary conditions can be set up for each axes individually, for example
70 |
71 | .. plot::
72 | :format: doctest
73 | :include-source:
74 | :context: close-figs
75 |
76 | >>> latt = lp.simple_square()
77 | >>> latt.build((6, 4))
78 | >>> latt.set_periodic(0)
79 | >>> latt.plot()
80 | >>> plt.show()
81 |
82 | or for multiple axes at once:
83 |
84 | .. plot::
85 | :format: doctest
86 | :include-source:
87 | :context: close-figs
88 |
89 | >>> latt = lp.simple_square()
90 | >>> latt.build((6, 4))
91 | >>> latt.set_periodic([0, 1])
92 | >>> latt.plot()
93 | >>> plt.show()
94 |
95 | The periodic boundary conditions are computed in the same coordinate system chosen for
96 | building the model. If ``primitive=False``, i.e. world coordinates, the box around
97 | the buildt lattice is repeated periodically:
98 |
99 | .. plot::
100 | :format: doctest
101 | :include-source:
102 | :context: close-figs
103 |
104 | >>> latt = lp.graphene()
105 | >>> latt.build((5.5, 4.5))
106 | >>> latt.set_periodic(0)
107 | >>> latt.plot()
108 | >>> plt.show()
109 |
110 | Here, the periodic boundary conditions again are set up along the x-axis, even though
111 | the basis vectors of the hexagonal lattice define a new basis. In the coordinate system
112 | of the lattice the periodic boundary contitions are set up along the basis vectors:
113 |
114 | .. plot::
115 | :format: doctest
116 | :include-source:
117 | :context: close-figs
118 |
119 | >>> latt = lp.graphene()
120 | >>> latt.build((5.5, 4.5), primitive=True)
121 | >>> latt.set_periodic(0)
122 | >>> latt.plot()
123 | >>> plt.show()
124 |
125 | .. warning::
126 | The ``set_periodic`` method assumes the lattice is build such that periodic
127 | boundary condtitions are possible. This is especially important if a lattice
128 | with multiple atoms in the unit cell is used. To correctly connect both sides of
129 | the lattice it has to be ensured that each cell in the lattice is fully contained.
130 | If, for example, the last unit cell in the x-direction is cut off in the middle
131 | no perdiodic boundary conditions will be computed since the distance between the
132 | two edges is larger than the other distances in the lattice.
133 | A future version will check if this requirement is fulfilled, but until now the
134 | user is responsible for the correct configuration.
135 |
136 |
137 |
138 | Position and neighbor data
139 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
140 |
141 | After building the lattice and optionally setting periodic boundary conditions the
142 | information of the buildt lattice can be accessed. The data of the
143 | lattice model then can be accessed by a simple index ``i``. The syntax is the same as
144 | before, just without the ``get_`` prefix. In order to find the right index,
145 | the ``plot`` method also supports showing the coorespnding super indices of the lattice sites:
146 |
147 | .. plot::
148 | :format: doctest
149 | :include-source:
150 | :context: close-figs
151 |
152 | >>> latt = lp.simple_square()
153 | >>> latt.build((6, 4))
154 | >>> latt.set_periodic(0)
155 | >>> latt.plot(show_indices=True)
156 | >>> plt.show()
157 |
158 | The positions of the sites in the model can now be accessed via the super index ``i``:
159 |
160 | >>> latt.position(2)
161 | [0. 2.]
162 |
163 | Similarly, the neighbors can be found via
164 |
165 | >>> latt.neighbors(2, distidx=0)
166 | [3 1 7 32]
167 |
168 | The nearest neighbors also can be found with the helper method
169 |
170 | >>> latt.nearest_neighbors(2)
171 | [3 1 7 32]
172 |
173 |
174 | The position and neighbor data of the finite lattice model is stored in the
175 | ``LatticeData`` object, wich can be accessed via the ``data`` attribute.
176 | Additionally, the positions and (lattice) indices of the model can be directly
177 | fetched, for example
178 |
179 | >>> latt.positions
180 | [[0. 0.]
181 | [0. 1.]
182 | ...
183 | [6. 3.]
184 | [6. 4.]]
185 |
186 |
187 | Data map
188 | ~~~~~~~~
189 |
190 |
191 | The lattice model makes it is easy to construct the (tight-binding) Hamiltonian of a non-interacting model:
192 |
193 | >>> latt = simple_chain(a=1.0)
194 | >>> latt.build(shape=4)
195 | >>> n = latt.num_sites
196 | >>> eps, t = 0., 1.
197 | >>> ham = np.zeros((n, n))
198 | >>> for i in range(n):
199 | ... ham[i, i] = eps
200 | ... for j in latt.nearest_neighbors(i):
201 | ... ham[i, j] = t
202 | >>> ham
203 | [[0. 1. 0. 0. 0.]
204 | [1. 0. 1. 0. 0.]
205 | [0. 1. 0. 1. 0.]
206 | [0. 0. 1. 0. 1.]
207 | [0. 0. 0. 1. 0.]]
208 |
209 |
210 | Since we loop over all sites of the lattice the construction of the hamiltonian is slow.
211 | An alternative way of mapping the lattice data to the hamiltonian is using the `DataMap`
212 | object returned by the `map()` method of the lattice data. This stores the atom-types,
213 | neighbor-pairs and corresponding distances of the lattice sites. Using the built-in
214 | masks the construction of the hamiltonian-data can be vectorized:
215 |
216 |
217 | >>> from scipy import sparse
218 | >>> eps, t = 0., 1.
219 | >>> dmap = latt.data.map() # Build datamap
220 | >>> values = np.zeros(dmap.size) # Initialize array for data of H
221 | >>> values[dmap.onsite(alpha=0)] = eps # Map onsite-energies to array
222 | >>> values[dmap.hopping(distidx=0)] = t # Map hopping-energies to array
223 | >>> ham_s = sparse.csr_matrix((values, dmap.indices))
224 | >>> ham_s.toarray()
225 | [[0. 1. 0. 0. 0.]
226 | [1. 0. 1. 0. 0.]
227 | [0. 1. 0. 1. 0.]
228 | [0. 0. 1. 0. 1.]
229 | [0. 0. 0. 1. 0.]]
230 |
--------------------------------------------------------------------------------
/docs/source/tutorial/general.rst:
--------------------------------------------------------------------------------
1 | General lattice attributes
2 | --------------------------
3 |
4 | After configuring the lattice the general attributes and methods are available.
5 | Even without building a (finite) lattice structure all properties can be computed on
6 | the fly for a given lattice vector, consisting of the translation vector ``n`` and
7 | the index ``alpha`` of the atom in the unit cell.
8 |
9 | We will discuss all properties with a simple hexagonal lattice as example:
10 |
11 | .. plot::
12 | :format: doctest
13 | :include-source:
14 | :context: close-figs
15 |
16 | >>> latt = lp.Lattice.hexagonal()
17 | >>> latt.add_atom()
18 | >>> latt.add_connections()
19 | >>> latt.plot_cell()
20 | >>> plt.show()
21 |
22 |
23 | Unit cell properties
24 | ~~~~~~~~~~~~~~~~~~~~
25 |
26 | The basis vectors of the lattice can be accessed via the ``vectors`` property:
27 |
28 | >>> latt.vectors
29 | [[ 1.5 0.8660254]
30 | [ 1.5 -0.8660254]]
31 |
32 | The size and volume of the unit cell defined by the basis vectors are also available:
33 |
34 | >>> latt.cell_size
35 | [1.5 1.73205081]
36 | >>> latt.cell_volume
37 | 2.598076211353316
38 |
39 | The results are all computed in cartesian corrdinates.
40 |
41 |
42 | Transformations and atom positions
43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
44 |
45 | Coordinates in *cartesian* coordinates (also referred to as *world* coordinates) can be
46 | tranformed to the *lattice* or *basis* coordinate system and vice versa. Consider the point
47 | :math:`\boldsymbol{n} = (n_1, \dots, n_d)` in the basis coordinate system, which can be
48 | understood as a translation vector. The point :math:`\boldsymbol{x} = (x_1, \dots, x_d)`
49 | in cartesian coordinates then is given by
50 |
51 | .. math::
52 | \boldsymbol{x} = \sum_{i=1}^d n_i \boldsymbol{a}_i.
53 |
54 | >>> n = [1, 0]
55 | >>> x = latt.transform(n)
56 | >>> x
57 | [1.5 0.8660254]
58 | >>> latt.itransform(x)
59 | [1. 0.]
60 |
61 | The points in the world coordinate system do not have to match the lattice points
62 | defined by the basis vectors:
63 |
64 | >>> latt.itransform([1.5, 0.0])
65 | [0.5 0.5]
66 |
67 | .. plot::
68 | :format: doctest
69 | :context: close-figs
70 |
71 | latt = lp.Lattice.hexagonal()
72 | ax = latt.plot_cell()
73 |
74 | ax.plot([1.5], [0.0], marker="x", color="r", ms=10)
75 | lp.plotting.draw_arrows(ax, 0.5 * latt.vectors[0], color="r", width=0.005)
76 | lp.plotting.draw_arrows(ax, [0.5 * latt.vectors[1]], pos=[0.5 * latt.vectors[0]], color="r", width=0.005)
77 | plt.show()
78 |
79 | Both methods are vectorized and support multiple points as inputs:
80 |
81 | >>> n = [[0, 0] [1, 0], [2, 0]]
82 | >>> x = latt.transform(n)
83 | >>> x
84 | [[0. 0. ]
85 | [1.5 0.8660254 ]
86 | [3. 1.73205081]]
87 | >>> latt.itransform(x)
88 | [[ 0.00000000e+00 0.00000000e+00]
89 | [ 1.00000000e+00 -3.82105486e-17]
90 | [ 2.00000000e+00 -7.64210971e-17]]
91 |
92 | .. note::
93 | As can be seen in the last example, some inaccuracies can occur in the
94 | transformations depending on the data type due to machine precision.
95 |
96 |
97 | Any point :math:`\boldsymbol{r}` in the cartesian cooridnates can be translated by a
98 | translation vector :math:`\boldsymbol{n} = (n_1, \dots, n_d)`:
99 |
100 | .. math::
101 | \boldsymbol{x} = \boldsymbol{r} + \sum_{i=1}^d n_i \boldsymbol{a}_i.
102 |
103 | The inverse operation is also available. It returns the translation vector
104 | :math:`\boldsymbol{n} = (n_1, \dots, n_d)` and the point :math:`\boldsymbol{r}` such that
105 | :math:`\boldsymbol{r}` is the neareast possible point to the origin:
106 |
107 | >>> n = [1, 0]
108 | >>> r = [0.5, 0.0]
109 | >>> x = latt.translate(n, r)
110 | >>> x
111 | [2. 0.8660254]
112 | >>> latt.itransform(x)
113 | (array([1, 0]), array([0.5, 0. ]))
114 |
115 | Again, both methods are vectorized:
116 |
117 | >>> n = [[0, 0], [1, 0], [2, 0]]
118 | >>> r = [0.5, 0]
119 | >>> x = latt.translate(n, r)
120 | >>> x
121 | [[0.5 0. ]
122 | [2. 0.8660254 ]
123 | [3.5 1.73205081]]
124 | >>> n2, r2 = latt.itranslate(x)
125 | >>> n2
126 | [[0 0]
127 | [1 0]
128 | [2 0]]
129 | >>> r2
130 | [[0.5 0. ]
131 | [0.5 0. ]
132 | [0.5 0. ]]
133 |
134 | Specifiying the index of the atom in the unit cell ``alpha`` the positions of a
135 | translated atom can be obtained via the translation vector :math:`\boldsymbol{n}`:
136 |
137 | >>> latt.get_position([0, 0], alpha=0)
138 | [0. 0.]
139 | >>> latt.get_position([1, 0], alpha=0)
140 | [1.5 0.8660254]
141 | >>> latt.get_position([2, 0], alpha=0)
142 | [3. 1.73205081]
143 |
144 | Multiple positions can be computed by the ``get_positions`` method. The argument is
145 | a list of lattice indices, consisting of the translation vector ``n`` and the atom index
146 | ``alpha`` as a single array. Note the last column of ``indices`` in the following
147 | example, where all atom indices ``alpha=0``:
148 |
149 | >>> indices = [[0, 0, 0], [1, 0, 0], [2, 0, 0]]
150 | >>> latt.get_positions(indices)
151 | [[0. 0. ]
152 | [1.5 0.8660254 ]
153 | [3. 1.73205081]]
154 |
155 |
156 | Neighbors
157 | ~~~~~~~~~
158 |
159 | The maximal number of neighbors of the atoms in the unit cell for *all* distance levels
160 | can be accessed by the property ``num_neighbors``:
161 |
162 | >>> latt.num_neighbors
163 | [6]
164 |
165 | Since the lattice only contains one atom in the unit cell a array with one element is
166 | returned. Similar to the position of a lattice site, the neighbors of a site
167 | can be obatined by the translation vector of the unit cell and the atom index.
168 | Additionaly, the distance level has to be specified via an index. The nearest
169 | neighbors of the site at the origin can, for example, be computed by calling
170 |
171 | >>> neighbors = latt.get_neighbors([1, 0], alpha=0, distidx=0)
172 | >>> neighbors
173 | [[ 2 -1 0]
174 | [ 0 1 0]
175 | [ 0 0 0]
176 | [ 2 0 0]
177 | [ 1 -1 0]
178 | [ 1 1 0]]
179 |
180 | The results ara again arrays cvontaining translation vectors plus the atom index ``alpha``:
181 |
182 | >>> neighbor = neighbors[0]
183 | >>> n, alpha = neighbor[:-1], neighbor[-1]
184 | >>> n
185 | [ 2 -1]
186 | >>> alpha
187 | 0
188 |
189 | In addition to the lattice indices the positions of the neighbors can be computed:
190 |
191 | >>> latt.get_neighbor_positions([1, 0], alpha=0, distidx=0)
192 | [[ 1.5 2.59807621]
193 | [ 1.5 -0.8660254 ]
194 | [ 0. 0. ]
195 | [ 3. 1.73205081]
196 | [ 0. 1.73205081]
197 | [ 3. 0. ]]
198 |
199 | or the vectors from the site to the neighbors
200 |
201 | >>> latt.get_neighbor_positions(alpha=0, distidx=0)
202 | [[ 0. 1.73205081]
203 | [ 0. -1.73205081]
204 | [-1.5 -0.8660254 ]
205 | [ 1.5 0.8660254 ]
206 | [-1.5 0.8660254 ]
207 | [ 1.5 -0.8660254 ]]
208 |
209 | Here no translation vector is needed since the vectors from a site to it's neighbors
210 | are translational invariant.
211 |
212 |
213 | Reciprocal lattice
214 | ~~~~~~~~~~~~~~~~~~
215 |
216 | The reciprocal lattice vectors of the ``Lattice`` instance can be computed via
217 |
218 | >>> latt.reciprocal_vectors()
219 | [[ 2.0943951 3.62759873]
220 | [ 2.0943951 -3.62759873]]
221 |
222 | Also, the reciprocal lattice can be constrcuted, which has the reciprocal vectors
223 | from the current lattice as basis vectors:
224 |
225 | >>> rlatt = latt.reciprocal_lattice()
226 | >>> rlatt.vectors
227 | [[ 2.0943951 3.62759873]
228 | [ 2.0943951 -3.62759873]]
229 |
230 | The reciprocal lattice can be used to construct the 1. Brillouin zone of a lattice,
231 | whcih is defined as the Wigner-Seitz cell of the reciprocal lattice:
232 |
233 | >>> bz = rlatt.wigner_seitz_cell()
234 |
235 | Additionally, a explicit method is available:
236 |
237 | >>> bz = latt.brillouin_zone()
238 |
239 | The 1. Brillouin zone can be visualized:
240 |
241 | .. plot::
242 | :format: doctest
243 | :include-source:
244 | :context: close-figs
245 |
246 | >>> latt = lp.Lattice.hexagonal()
247 | >>> bz = latt.brillouin_zone()
248 | >>> bz.draw()
249 | >>> plt.show()
250 |
--------------------------------------------------------------------------------
/docs/source/tutorial/index.rst:
--------------------------------------------------------------------------------
1 | Tutorial
2 | ========
3 |
4 | In this tutorial the main features and usecases of LattPy are discussed and explained.
5 | Throughout the tutorial the packages ``numpy`` and ``matplotlib`` are used. `LattPy`
6 | is imported as ``lp`` - using a similar alias as the other scientific computing libaries:
7 |
8 | >>> import numpy as np
9 | >>> import matplotlib.pyplot as plt
10 | >>> import lattpy as lp
11 |
12 |
13 | .. toctree::
14 | :maxdepth: 3
15 | :numbered:
16 |
17 | configuration
18 | general
19 | finite
20 |
--------------------------------------------------------------------------------
/lattpy/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | """Package for modeling Bravais lattices and finite lattice structures.
12 |
13 | Submodules
14 | ----------
15 |
16 | .. autosummary::
17 | lattpy.atom
18 | lattpy.basis
19 | lattpy.data
20 | lattpy.disptools
21 | lattpy.lattice
22 | lattpy.shape
23 | lattpy.spatial
24 | lattpy.structure
25 | lattpy.utils
26 |
27 | """
28 |
29 | from .utils import (
30 | logger,
31 | ArrayLike,
32 | LatticeError,
33 | ConfigurationError,
34 | SiteOccupiedError,
35 | NoConnectionsError,
36 | NotAnalyzedError,
37 | NotBuiltError,
38 | min_dtype,
39 | chain,
40 | frmt_num,
41 | frmt_bytes,
42 | frmt_time,
43 | )
44 |
45 | from .spatial import (
46 | distance,
47 | distances,
48 | interweave,
49 | vindices,
50 | vrange,
51 | cell_size,
52 | cell_volume,
53 | compute_vectors,
54 | VoronoiTree,
55 | WignerSeitzCell,
56 | )
57 |
58 | from .shape import AbstractShape, Shape, Circle, Donut, ConvexHull
59 | from .data import LatticeData
60 | from .disptools import DispersionPath
61 | from .atom import Atom
62 | from .basis import LatticeBasis
63 | from .structure import LatticeStructure
64 | from .lattice import Lattice
65 |
66 | try:
67 | from ._version import version as __version__
68 | except ImportError:
69 | __version__ = "0.0.0"
70 |
71 |
72 | # =========================================================================
73 | # 1D Lattices
74 | # =========================================================================
75 |
76 |
77 | def simple_chain(a=1.0, atom=None, neighbors=1):
78 | """Creates a 1D lattice with one atom at the origin of the unit cell.
79 |
80 | Parameters
81 | ----------
82 | a : float, optional
83 | The lattice constant (length of the basis-vector).
84 | atom : str or Atom, optional
85 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
86 | is created.
87 | neighbors : int, optional
88 | The number of neighbor-distance levels, e.g. setting to 1 means only
89 | nearest neighbors. The default is nearest neighbors (1).
90 |
91 | Returns
92 | -------
93 | latt : Lattice
94 | The configured lattice instance.
95 |
96 | Examples
97 | --------
98 | >>> import matplotlib.pyplot as plt
99 | >>> latt = lp.simple_chain()
100 | >>> latt.plot_cell()
101 | >>> plt.show()
102 |
103 | """
104 | latt = Lattice.chain(a)
105 | latt.add_atom(atom=atom)
106 | latt.add_connections(neighbors)
107 | return latt
108 |
109 |
110 | def alternating_chain(a=1.0, atom1=None, atom2=None, x0=0.0, neighbors=1):
111 | """Creates a 1D lattice with two atoms in the unit cell.
112 |
113 | Parameters
114 | ----------
115 | a : float, optional
116 | The lattice constant (length of the basis-vector).
117 | atom1 : str or Atom, optional
118 | The first atom to add to the lattice. If a string is passed, a new
119 | ``Atom`` instance is created.
120 | atom2 : str or Atom, optional
121 | The second atom to add to the lattice. If a string is passed, a new
122 | ``Atom`` instance is created.
123 | x0 : float, optional
124 | The offset of the atom positions in x-direction.
125 | neighbors : int, optional
126 | The number of neighbor-distance levels, e.g. setting to 1 means only
127 | nearest neighbors. The default is nearest neighbors (1).
128 |
129 | Returns
130 | -------
131 | latt : Lattice
132 | The configured lattice instance.
133 |
134 | Examples
135 | --------
136 | >>> import matplotlib.pyplot as plt
137 | >>> latt = lp.alternating_chain()
138 | >>> latt.plot_cell()
139 | >>> plt.show()
140 |
141 | """
142 | latt = Lattice.chain(a)
143 | latt.add_atom(pos=(0.0 + x0) * a, atom=atom1)
144 | latt.add_atom(pos=(0.5 + x0) * a, atom=atom2)
145 | latt.add_connections(neighbors)
146 | return latt
147 |
148 |
149 | # =========================================================================
150 | # 2D Lattices
151 | # =========================================================================
152 |
153 |
154 | def simple_square(a=1.0, atom=None, neighbors=1):
155 | """Creates a square lattice with one atom at the origin of the unit cell.
156 |
157 | Parameters
158 | ----------
159 | a : float, optional
160 | The lattice constant (length of the basis-vector).
161 | atom : str or Atom, optional
162 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
163 | is created.
164 | neighbors : int, optional
165 | The number of neighbor-distance levels, e.g. setting to 1 means only
166 | nearest neighbors. The default is nearest neighbors (1).
167 |
168 | Returns
169 | -------
170 | latt : Lattice
171 | The configured lattice instance.
172 |
173 | Examples
174 | --------
175 | >>> import matplotlib.pyplot as plt
176 | >>> latt = lp.simple_square()
177 | >>> latt.plot_cell()
178 | >>> plt.show()
179 |
180 | """
181 | latt = Lattice.square(a)
182 | latt.add_atom(atom=atom)
183 | latt.add_connections(neighbors)
184 | return latt
185 |
186 |
187 | def simple_rectangular(a1=1.5, a2=1.0, atom=None, neighbors=2):
188 | """Creates a rectangular lattice with one atom at the origin of the unit cell.
189 |
190 | Parameters
191 | ----------
192 | a1 : float, optional
193 | The lattice constant in the x-direction.
194 | a2 : float, optional
195 | The lattice constant in the y-direction.
196 | atom : str or Atom, optional
197 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
198 | is created.
199 | neighbors : int, optional
200 | The number of neighbor-distance levels, e.g. setting to 1 means only
201 | nearest neighbors. The default is nearest neighbors (2).
202 |
203 | Returns
204 | -------
205 | latt : Lattice
206 | The configured lattice instance.
207 |
208 | Examples
209 | --------
210 | >>> import matplotlib.pyplot as plt
211 | >>> latt = lp.simple_rectangular()
212 | >>> latt.plot_cell()
213 | >>> plt.show()
214 |
215 | """
216 | latt = Lattice.rectangular(a1, a2)
217 | latt.add_atom(atom=atom)
218 | latt.add_connections(neighbors)
219 | return latt
220 |
221 |
222 | def simple_hexagonal(a=1.0, atom=None, neighbors=1):
223 | """Creates a hexagonal lattice with one atom at the origin of the unit cell.
224 |
225 | Parameters
226 | ----------
227 | a : float, optional
228 | The lattice constant (length of the basis-vector).
229 | atom : str or Atom, optional
230 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
231 | is created.
232 | neighbors : int, optional
233 | The number of neighbor-distance levels, e.g. setting to 1 means only
234 | nearest neighbors. The default is nearest neighbors (1).
235 |
236 | Returns
237 | -------
238 | latt : Lattice
239 | The configured lattice instance.
240 |
241 | Examples
242 | --------
243 | >>> import matplotlib.pyplot as plt
244 | >>> latt = lp.simple_hexagonal()
245 | >>> latt.plot_cell()
246 | >>> plt.show()
247 |
248 | """
249 | latt = Lattice.hexagonal(a)
250 | latt.add_atom(atom=atom)
251 | latt.add_connections(neighbors)
252 | return latt
253 |
254 |
255 | def honeycomb(a=1.0, atom=None):
256 | """Creates a honeycomb lattice with two identical atoms in the unit cell.
257 |
258 | Parameters
259 | ----------
260 | a : float, optional
261 | The lattice constant (length of the basis-vector).
262 | atom : str or Atom, optional
263 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
264 | is created.
265 |
266 | Returns
267 | -------
268 | latt : Lattice
269 | The configured lattice instance.
270 |
271 | Examples
272 | --------
273 | >>> import matplotlib.pyplot as plt
274 | >>> latt = lp.honeycomb()
275 | >>> latt.plot_cell()
276 | >>> plt.show()
277 |
278 | """
279 | if not isinstance(atom, Atom):
280 | atom = Atom(atom, color="C0")
281 | latt = Lattice.hexagonal(a)
282 | latt.add_atom([0, 0], atom=atom)
283 | latt.add_atom([a, 0], atom=atom)
284 | latt.add_connection(0, 1, analyze=True)
285 | return latt
286 |
287 |
288 | def graphene(a=1.0):
289 | """Creates a hexagonal lattice with two atoms in the unit cell.
290 |
291 | Parameters
292 | ----------
293 | a : float, optional
294 | The lattice constant (length of the basis-vectors).
295 |
296 | Returns
297 | -------
298 | latt : Lattice
299 | The configured lattice instance.
300 |
301 | Examples
302 | --------
303 | >>> import matplotlib.pyplot as plt
304 | >>> latt = lp.graphene()
305 | >>> latt.plot_cell()
306 | >>> plt.show()
307 |
308 | """
309 | at1 = Atom("C1")
310 | at2 = Atom("C2")
311 | latt = Lattice.hexagonal(a)
312 | latt.add_atom([0, 0], at1)
313 | latt.add_atom([a, 0], at2)
314 | latt.add_connection(at1, at2, analyze=True)
315 | return latt
316 |
317 |
318 | # =========================================================================
319 | # 3D Lattices
320 | # =========================================================================
321 |
322 |
323 | def simple_cubic(a=1.0, atom=None, neighbors=1):
324 | """Creates a cubic lattice with one atom at the origin of the unit cell.
325 |
326 | Parameters
327 | ----------
328 | a : float, optional
329 | The lattice constant (length of the basis-vector).
330 | atom : str or Atom, optional
331 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
332 | is created.
333 | neighbors : int, optional
334 | The number of neighbor-distance levels, e.g. setting to 1 means only
335 | nearest neighbors. The default is nearest neighbors (1).
336 |
337 | Returns
338 | -------
339 | latt : Lattice
340 | The configured lattice instance.
341 |
342 | Examples
343 | --------
344 | >>> import matplotlib.pyplot as plt
345 | >>> latt = lp.simple_cubic()
346 | >>> latt.plot_cell()
347 | >>> plt.show()
348 |
349 | """
350 | latt = Lattice.sc(a)
351 | latt.add_atom(atom=atom)
352 | latt.add_connections(neighbors)
353 | return latt
354 |
355 |
356 | def nacl_structure(a=1.0, atom1="Na", atom2="Cl", neighbors=1):
357 | """Creates a NaCl lattice structure.
358 |
359 | Parameters
360 | ----------
361 | a : float, optional
362 | The lattice constant (length of the basis-vector).
363 | atom1 : str or Atom, optional
364 | The first atom to add to the lattice. If a string is passed, a new
365 | ``Atom`` instance is created. The default name is `Na`.
366 | atom2 : str or Atom, optional
367 | The second atom to add to the lattice. If a string is passed, a new
368 | ``Atom`` instance is created.. The default name is `Cl`.
369 | neighbors : int, optional
370 | The number of neighbor-distance levels, e.g. setting to 1 means only
371 | nearest neighbors. The default is nearest neighbors (1).
372 |
373 | Returns
374 | -------
375 | latt : Lattice
376 | The configured lattice instance.
377 |
378 | Examples
379 | --------
380 | >>> import matplotlib.pyplot as plt
381 | >>> latt = lp.nacl_structure()
382 | >>> latt.plot_cell()
383 | >>> plt.show()
384 |
385 | """
386 | latt = Lattice.fcc(a)
387 | latt.add_atom(pos=[0, 0, 0], atom=atom1)
388 | latt.add_atom(pos=[a / 2, a / 2, a / 2], atom=atom2)
389 | latt.add_connections(neighbors)
390 | return latt
391 |
392 |
393 | # ======================================================================================
394 | # Other
395 | # ======================================================================================
396 |
397 |
398 | def finite_hypercubic(s, a=1.0, atom=None, neighbors=1, primitive=True, periodic=None):
399 | """Creates a d-dimensional finite lattice model with one atom in the unit cell.
400 |
401 | Parameters
402 | ----------
403 | s : float or Sequence[float] or AbstractShape
404 | The shape of the finite lattice. This also defines the dimensionality.
405 | a : float, optional
406 | The lattice constant (length of the basis-vectors).
407 | atom : str or Atom, optional
408 | The atom to add to the lattice. If a string is passed, a new ``Atom`` instance
409 | is created.
410 | neighbors : int, optional
411 | The number of neighbor-distance levels, e.g. setting to 1 means only
412 | nearest neighbors. The default is nearest neighbors (1).
413 | primitive : bool, optional
414 | If True the shape will be multiplied by the cell size of the model.
415 | The default is True.
416 | periodic : bool or int or (N, ) array_like
417 | One or multiple axises to apply the periodic boundary conditions.
418 | If the axis is ``None`` no perodic boundary conditions will be set.
419 |
420 | Returns
421 | -------
422 | latt : Lattice
423 | The configured lattice instance.
424 |
425 | Examples
426 | --------
427 | Simple chain:
428 |
429 | >>> import matplotlib.pyplot as plt
430 | >>> latt = lp.finite_hypercubic(4)
431 | >>> latt.plot()
432 | >>> plt.show()
433 |
434 | Simple square:
435 |
436 | >>> import matplotlib.pyplot as plt
437 | >>> latt = lp.finite_hypercubic((4, 2))
438 | >>> latt.plot()
439 | >>> plt.show()
440 |
441 | """
442 | import numpy as np
443 |
444 | if isinstance(s, (float, int)):
445 | s = (s,)
446 |
447 | dim = s.dim if isinstance(s, AbstractShape) else len(s)
448 | latt = Lattice(a * np.eye(dim))
449 | latt.add_atom(atom=atom)
450 | latt.add_connections(neighbors)
451 | latt.build(s, primitive=primitive, periodic=periodic)
452 | return latt
453 |
--------------------------------------------------------------------------------
/lattpy/atom.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | """Objects for representing atoms and the unitcell of a lattice."""
12 |
13 | import itertools
14 | from collections import abc
15 | from typing import Union, Any, Iterator, Dict
16 |
17 | __all__ = ["Atom"]
18 |
19 |
20 | class Atom(abc.MutableMapping):
21 | """Object representing an atom of a Bravais lattice.
22 |
23 | Parameters
24 | ----------
25 | name : str, optional
26 | The name of the atom. The default is 'A'.
27 | radius : float, optional
28 | The radius of the atom in real space.
29 | color : str or float or array_like, optional
30 | The color used to visualize the atom.
31 | weight : float, optional
32 | The weight of the atom.
33 | **kwargs
34 | Additional attributes of the atom.
35 | """
36 |
37 | _counter = itertools.count()
38 |
39 | __slots__ = ["_index", "_name", "_weight", "_params"]
40 |
41 | def __init__(
42 | self,
43 | name: str = None,
44 | radius: float = 0.2,
45 | color: str = None,
46 | weight: float = 1.0,
47 | **kwargs,
48 | ):
49 | super().__init__()
50 | index = next(Atom._counter)
51 | self._index = index
52 | self._name = name or "A"
53 | self._weight = weight
54 | self._params = dict(color=color, radius=radius, **kwargs)
55 |
56 | @property
57 | def id(self):
58 | """The id of the atom."""
59 | return id(self)
60 |
61 | @property
62 | def index(self) -> int:
63 | """Return the index of the ``Atom`` instance."""
64 | return self._index
65 |
66 | @property
67 | def name(self) -> str:
68 | """Return the name of the ``Atom`` instance."""
69 | return self._name
70 |
71 | @property
72 | def weight(self):
73 | """Return the weight or the ``Atom`` instance."""
74 | return self._weight
75 |
76 | def dict(self) -> Dict[str, Any]:
77 | """Returns the data of the ``Atom`` instance as a dictionary."""
78 | data = dict(index=self._index, name=self._name)
79 | data.update(self._params)
80 | return data
81 |
82 | def copy(self) -> "Atom":
83 | """Creates a deep copy of the ``Atom`` instance."""
84 | return Atom(self.name, weight=self.weight, **self._params.copy())
85 |
86 | def get(self, key: str, default=None) -> Any:
87 | try:
88 | return self.__getitem__(key)
89 | except KeyError:
90 | return default
91 |
92 | def is_identical(self, other: "Atom") -> bool:
93 | """Checks if the other ``Atom`` is identical to this one."""
94 | return self._name == other.name
95 |
96 | def __len__(self) -> int:
97 | """Return the length of the ``Atom`` attributes."""
98 | return len(self._params)
99 |
100 | def __iter__(self) -> Iterator[str]:
101 | """Iterate over the keys of the ``Atom`` attributes."""
102 | return iter(self._params)
103 |
104 | def __getitem__(self, key: str) -> Any:
105 | """Make ``Atom`` attributes accessable as dictionary items."""
106 | return self._params[key]
107 |
108 | def __setitem__(self, key: str, value: Any) -> None:
109 | """Make ``Atom`` attributes accessable as dictionary items."""
110 | self._params[key] = value
111 |
112 | def __delitem__(self, key: str) -> None:
113 | """Make ``Atom`` attributes accessable as dictionary items."""
114 | del self._params[key]
115 |
116 | def __getattribute__(self, key: str) -> Any:
117 | """Make ``Atom`` attributes accessable as attributes."""
118 | key = str(key)
119 | if not key.startswith("_") and key in self._params.keys():
120 | return self._params[key]
121 | else:
122 | return super().__getattribute__(key)
123 |
124 | def __setattr__(self, key: str, value: Any) -> None:
125 | """Make ``Atom`` attributes accessable as attributes."""
126 | key = str(key)
127 | if not key.startswith("_") and key in self._params.keys():
128 | self._params[key] = value
129 | else:
130 | super().__setattr__(key, value)
131 |
132 | def __hash__(self) -> hash:
133 | """Make ``Atom`` instance hashable."""
134 | return hash(self._name)
135 |
136 | def __dict__(self) -> Dict[str, Any]: # pragma: no cover
137 | """Return the information of the atom as a dictionary"""
138 | return self.dict()
139 |
140 | def __copy__(self) -> "Atom": # pragma: no cover
141 | """Creates a deep copy of the ``Atom`` instance."""
142 | return self.copy()
143 |
144 | def __eq__(self, other: Union["Atom", str]) -> bool:
145 | if isinstance(other, Atom):
146 | return self.is_identical(other)
147 | else:
148 | return self._name == other
149 |
150 | def __repr__(self) -> str:
151 | argstr = f"{self._name}"
152 | paramstr = ", ".join(f"{k}={v}" for k, v in self._params.items() if v)
153 | if paramstr:
154 | argstr += ", " + paramstr
155 | return f"Atom({argstr}, {self.index})"
156 |
--------------------------------------------------------------------------------
/lattpy/disptools.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | """Tools for dispersion computation and plotting."""
12 |
13 | import numpy as np
14 | import matplotlib.pyplot as plt
15 | import matplotlib.ticker as tck
16 | from .utils import chain
17 | from .spatial import distance
18 | from .plotting import draw_lines
19 | from .atom import Atom
20 |
21 | __all__ = [
22 | "bandpath_subplots",
23 | "plot_dispersion",
24 | "disp_dos_subplots",
25 | "plot_disp_dos",
26 | "plot_bands",
27 | "DispersionPath",
28 | ]
29 |
30 |
31 | def _color_list(color, num_bands):
32 | if color is None:
33 | colors = [f"C{i}" for i in range(num_bands)]
34 | elif isinstance(color, str) or not hasattr(color, "__len__"):
35 | colors = [color] * num_bands
36 | else:
37 | colors = color
38 | return colors
39 |
40 |
41 | def _scale_xaxis(num_points, disp, scales=None):
42 | sect_size = len(disp) / (num_points - 1)
43 | scales = np.ones(num_points - 1) if scales is None else scales
44 | k0, k, ticks = 0, [], [0]
45 | for scale in scales:
46 | k.extend(k0 + np.arange(sect_size) * scale)
47 | k0 = k[-1]
48 | ticks.append(k0)
49 | return k, ticks
50 |
51 |
52 | def _set_piticks(axis, num_ticks=2, frmt=".1f"):
53 | axis.set_major_formatter(tck.FormatStrFormatter(rf"%{frmt} $\pi$"))
54 | axis.set_major_locator(tck.LinearLocator(2 * num_ticks + 1))
55 |
56 |
57 | def bandpath_subplots(ticks, labels, xlabel="$k$", ylabel="$E(k)$", grid="both"):
58 | fig, ax = plt.subplots()
59 | ax.set_xlim(0, ticks[-1])
60 | ax.set_xticks(ticks)
61 | ax.set_xticklabels(labels)
62 | if xlabel:
63 | ax.set_xlabel(xlabel)
64 | if ylabel:
65 | ax.set_ylabel(ylabel)
66 | if grid:
67 | if not isinstance(grid, str):
68 | grid = "both"
69 | ax.set_axisbelow(True)
70 | ax.grid(b=True, which="major", axis=grid)
71 | return fig, ax
72 |
73 |
74 | def _draw_dispersion(ax, k, disp, color=None, fill=False, alpha=0.2, lw=1.0):
75 | x = [0, np.max(k)]
76 | colors = _color_list(color, disp.shape[1])
77 | for i, band in enumerate(disp.T):
78 | col = colors[i]
79 | if isinstance(col, Atom):
80 | col = col.color
81 | ax.plot(k, band, lw=lw, color=col)
82 | if fill:
83 | ax.fill_between(x, min(band), max(band), color=col, alpha=alpha)
84 |
85 |
86 | def plot_dispersion(
87 | disp,
88 | labels,
89 | xlabel="$k$",
90 | ylabel="$E(k)$",
91 | grid="both",
92 | color=None,
93 | alpha=0.2,
94 | lw=1.0,
95 | scales=None,
96 | fill=False,
97 | ax=None,
98 | show=True,
99 | ):
100 | num_points = len(labels)
101 | k, ticks = _scale_xaxis(num_points, disp, scales)
102 | if ax is None:
103 | fig, ax = bandpath_subplots(ticks, labels, xlabel, ylabel, grid)
104 | else:
105 | fig = ax.get_figure()
106 |
107 | x = [0, np.max(k)]
108 | colors = _color_list(color, disp.shape[1])
109 | for i, band in enumerate(disp.T):
110 | col = colors[i]
111 | if isinstance(col, Atom):
112 | col = col.color
113 | ax.plot(k, band, lw=lw, color=col)
114 | if fill:
115 | ax.fill_between(x, min(band), max(band), color=col, alpha=alpha)
116 |
117 | fig.tight_layout()
118 | if show:
119 | plt.show()
120 | return ax
121 |
122 |
123 | def disp_dos_subplots(
124 | ticks,
125 | labels,
126 | xlabel="$k$",
127 | ylabel="$E(k)$",
128 | doslabel="$n(E)$",
129 | wratio=(3, 1),
130 | grid="both",
131 | ):
132 | fig, axs = plt.subplots(1, 2, gridspec_kw={"width_ratios": wratio}, sharey="all")
133 | ax1, ax2 = axs
134 | ax1.set_xlim(0, ticks[-1])
135 | if xlabel:
136 | ax1.set_xlabel(xlabel)
137 | if ylabel:
138 | ax1.set_ylabel(ylabel)
139 | if doslabel:
140 | ax2.set_xlabel(doslabel)
141 | ax1.set_xticks(ticks)
142 | ax1.set_xticklabels(labels)
143 | ax2.set_xticks([0])
144 | if grid:
145 | ax1.set_axisbelow(True)
146 | ax1.grid(b=True, which="major", axis=grid)
147 | ax2.set_axisbelow(True)
148 | ax2.grid(b=True, which="major", axis=grid)
149 | return fig, axs
150 |
151 |
152 | def plot_disp_dos(
153 | disp,
154 | dos_data,
155 | labels,
156 | xlabel="k",
157 | ylabel="E(k)",
158 | doslabel="n(E)",
159 | wratio=(3, 1),
160 | grid="both",
161 | color=None,
162 | fill=True,
163 | disp_alpha=0.2,
164 | dos_alpha=0.2,
165 | lw=1.0,
166 | scales=None,
167 | axs=None,
168 | show=True,
169 | ):
170 | num_points = len(labels)
171 | k, ticks = _scale_xaxis(num_points, disp, scales)
172 | if axs is None:
173 | fig, axs = disp_dos_subplots(
174 | ticks, labels, xlabel, ylabel, doslabel, wratio, grid
175 | )
176 | ax1, ax2 = axs
177 | else:
178 | ax1, ax2 = axs
179 | fig = ax1.get_figure()
180 |
181 | x = [0, np.max(k)]
182 | colors = _color_list(color, disp.shape[1])
183 | for i, band in enumerate(disp.T):
184 | col = colors[i]
185 | if isinstance(col, Atom):
186 | col = col.color
187 | ax1.plot(k, band, lw=lw, color=col)
188 | if fill:
189 | ax1.fill_between(x, min(band), max(band), color=col, alpha=disp_alpha)
190 |
191 | for i, band in enumerate(dos_data):
192 | col = colors[i]
193 | if isinstance(col, Atom):
194 | col = col.color
195 | bins, dos = band
196 | ax2.plot(dos, bins, lw=lw, color=col)
197 | ax2.fill_betweenx(bins, 0, dos, alpha=dos_alpha, color=col)
198 |
199 | ax2.set_xlim(0, ax2.get_xlim()[1])
200 | fig.tight_layout()
201 | if show:
202 | plt.show()
203 | return axs
204 |
205 |
206 | def plot_bands(
207 | kgrid,
208 | bands,
209 | k_label="k",
210 | disp_label="E(k)",
211 | grid="both",
212 | contour_grid=False,
213 | bz=None,
214 | pi_ticks=True,
215 | ax=None,
216 | show=True,
217 | ):
218 | if ax is None:
219 | fig, ax = plt.subplots()
220 | else:
221 | fig = ax.get_figure()
222 |
223 | dim = len(bands.shape) - 1
224 | if dim == 1:
225 | k = kgrid[0]
226 | ax.plot(k, bands.T)
227 | if k_label:
228 | ax.set_xlabel(f"${k_label}$")
229 | if disp_label:
230 | ax.set_ylabel(f"${disp_label}$")
231 | if grid:
232 | ax.grid(axis=grid)
233 |
234 | if pi_ticks:
235 | _set_piticks(ax.xaxis, num_ticks=2)
236 | ax.set_xlim(np.min(k), np.max(k))
237 |
238 | if bz is not None:
239 | for x in bz:
240 | ax.axvline(x=x, color="k")
241 |
242 | elif dim == 2:
243 | kx, ky = kgrid
244 | kxx, kyy = np.meshgrid(kx, ky)
245 | if len(bands) == 1:
246 | bands = bands[0]
247 | else:
248 | bands = np.sum(np.abs(bands), axis=0)
249 |
250 | im = ax.contourf(kxx, kyy, bands)
251 | ax.set_aspect("equal")
252 | if k_label:
253 | ax.set_xlabel(f"{k_label}$_x$")
254 | ax.set_ylabel(f"{k_label}$_y$")
255 | if disp_label:
256 | label = ""
257 | if disp_label:
258 | label = disp_label if len(bands) == 1 else f"|{disp_label}|"
259 | fig.colorbar(im, ax=ax, label=label)
260 | if grid and contour_grid:
261 | ax.grid(axis=grid)
262 |
263 | if pi_ticks:
264 | _set_piticks(ax.xaxis, num_ticks=2)
265 | _set_piticks(ax.yaxis, num_ticks=2)
266 |
267 | if bz is not None:
268 | draw_lines(ax, bz, color="k")
269 | else:
270 | raise NotImplementedError()
271 |
272 | fig.tight_layout()
273 | if show:
274 | plt.show()
275 | return ax
276 |
277 |
278 | class DispersionPath:
279 | """Defines a dispersion path between high symmetry (HS) points.
280 |
281 | Examples
282 | --------
283 | Define a path using the add-method or preset points. To get the actual points the
284 | 'build'-method is called:
285 |
286 | >>> path = DispersionPath(dim=3).add([0, 0, 0], 'Gamma').x(a=1.0).cycle()
287 | >>> vectors = path.build(n_sect=1000)
288 |
289 | Attributes
290 | ----------
291 | dim : int
292 | labels : list of str
293 | points : list of array_like
294 | n_sect : int
295 | """
296 |
297 | def __init__(self, dim=0):
298 | self.dim = dim
299 | self.labels = list()
300 | self.points = list()
301 | self.n_sect = 0
302 |
303 | @classmethod
304 | def chain_path(cls, a=1.0):
305 | return cls(dim=1).x(a).gamma().cycle()
306 |
307 | @classmethod
308 | def square_path(cls, a=1.0):
309 | return cls(dim=2).gamma().x(a).m(a).cycle()
310 |
311 | @classmethod
312 | def cubic_path(cls, a=1.0):
313 | return cls(dim=3).gamma().x(a).m(a).gamma().r(a)
314 |
315 | @property
316 | def num_points(self):
317 | """int: Number of HS points in the path"""
318 | return len(self.points)
319 |
320 | def add(self, point, name=""):
321 | """Adds a new HS point to the path
322 |
323 | This method returns the instance for easier path definitions.
324 |
325 | Parameters
326 | ----------
327 | point: array_like
328 | The coordinates of the HS point. If the dimension of the point is
329 | higher than the set dimension the point will be clipped.
330 | name: str, optional
331 | Optional name of the point. If not specified the number of the point
332 | is used.
333 |
334 | Returns
335 | -------
336 | self: DispersionPath
337 | """
338 | if not name:
339 | name = str(len(self.points))
340 | point = np.asarray(point)
341 | if self.dim:
342 | point = point[: self.dim]
343 | else:
344 | self.dim = len(point)
345 | self.points.append(point)
346 | self.labels.append(name)
347 | return self
348 |
349 | def add_points(self, points, names=None):
350 | """Adds multiple HS points to the path
351 |
352 | Parameters
353 | ----------
354 | points: array_like
355 | The coordinates of the HS points.
356 | names: list of str, optional
357 | Optional names of the points. If not specified the number of the point
358 | is used.
359 |
360 | Returns
361 | -------
362 | self: DispersionPath
363 | """
364 | if names is None:
365 | names = [""] * len(points)
366 | for point, name in zip(points, names):
367 | self.add(point, name)
368 | return self
369 |
370 | def cycle(self):
371 | """Adds the first point of the path.
372 |
373 | This method returns the instance for easier path definitions.
374 |
375 | Returns
376 | -------
377 | self: DispersionPath
378 | """
379 | self.points.append(self.points[0])
380 | self.labels.append(self.labels[0])
381 | return self
382 |
383 | def gamma(self):
384 | r"""DispersionPath: Adds the .math:'\Gamma=(0, 0, 0)' point to the path"""
385 | return self.add([0, 0, 0], r"$\Gamma$")
386 |
387 | def x(self, a=1.0):
388 | r"""DispersionPath: Adds the .math:'X=(\pi, 0, 0)' point to the path"""
389 | return self.add([np.pi / a, 0, 0], r"$X$")
390 |
391 | def m(self, a=1.0):
392 | r"""DispersionPath: Adds the ,math:'M=(\pi, \pi, 0)' point to the path"""
393 | return self.add([np.pi / a, np.pi / a, 0], r"$M$")
394 |
395 | def r(self, a=1.0):
396 | r"""DispersionPath: Adds the .math:'R=(\pi, \pi, \pi)' point to the path"""
397 | return self.add([np.pi / a, np.pi / a, np.pi / a], r"$R$")
398 |
399 | def build(self, n_sect=1000):
400 | """Builds the vectors defining the path between the set HS points.
401 |
402 | Parameters
403 | ----------
404 | n_sect: int, optional
405 | Number of points between each pair of HS points.
406 |
407 | Returns
408 | -------
409 | path: (N, D) np.ndarray
410 | """
411 | self.n_sect = n_sect
412 | path = np.zeros((0, self.dim))
413 | for p0, p1 in chain(self.points):
414 | path = np.append(path, np.linspace(p0, p1, num=n_sect), axis=0)
415 | return path
416 |
417 | def get_ticks(self):
418 | """Get the positions of the points of the last buildt path.
419 |
420 | Mainly used for setting ticks in plot.
421 |
422 | Returns
423 | -------
424 | ticks: (N) np.ndarray
425 | labels: (N) list
426 | """
427 | return np.arange(self.num_points) * self.n_sect, self.labels
428 |
429 | def edges(self):
430 | """Constructs the edges of the path."""
431 | return list(chain(self.points))
432 |
433 | def distances(self):
434 | """Computes the distances between the edges of the path."""
435 | dists = list()
436 | for p0, p1 in self.edges():
437 | dists.append(distance(p0, p1))
438 | return np.array(dists)
439 |
440 | def scales(self):
441 | """Computes the scales of the the edges of the path."""
442 | dists = self.distances()
443 | return dists / dists[0]
444 |
445 | def draw(self, ax, color=None, lw=1.0, **kwargs):
446 | lines = draw_lines(ax, self.edges(), color=color, lw=lw, **kwargs)
447 | return lines
448 |
449 | def subplots(self, xlabel="k", ylabel="E(k)", grid="both"):
450 | """Creates an empty matplotlib plot with configured axes for the path.
451 |
452 | Parameters
453 | ----------
454 | xlabel: str, optional
455 | ylabel: str, optional
456 | grid: str, optional
457 |
458 | Returns
459 | -------
460 | fig: plt.Figure
461 | ax: plt.Axis
462 | """
463 | ticks, labels = self.get_ticks()
464 | return bandpath_subplots(ticks, labels, xlabel, ylabel, grid)
465 |
466 | def plot_dispersion(self, disp, ax=None, show=True, **kwargs):
467 | scales = self.scales()
468 | return plot_dispersion(
469 | disp, self.labels, scales=scales, ax=ax, show=show, **kwargs
470 | )
471 |
472 | def plot_disp_dos(self, disp, dos, axs=None, show=True, **kwargs):
473 | scales = self.scales()
474 | return plot_disp_dos(
475 | disp, dos, self.labels, scales=scales, axs=axs, show=show, **kwargs
476 | )
477 |
--------------------------------------------------------------------------------
/lattpy/shape.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | """Objects for representing the shape of a finite lattice."""
12 |
13 | import numpy as np
14 | import scipy.spatial
15 | import matplotlib.pyplot as plt
16 | import itertools
17 | from abc import ABC, abstractmethod
18 | from .plotting import draw_lines, draw_surfaces
19 |
20 |
21 | class AbstractShape(ABC):
22 | """Abstract shape object."""
23 |
24 | def __init__(self, dim, pos=None):
25 | self.dim = dim
26 | self.pos = np.zeros(dim) if pos is None else np.array(pos)
27 |
28 | @abstractmethod
29 | def limits(self):
30 | """Returns the limits of the shape."""
31 | pass
32 |
33 | @abstractmethod
34 | def contains(self, points, tol=0.0):
35 | """Checks if the given points are contained in the shape."""
36 | pass
37 |
38 | @abstractmethod
39 | def plot(self, ax, color="k", lw=0.0, alpha=0.2, **kwargs):
40 | """Plots the contour of the shape."""
41 | pass
42 |
43 | def __repr__(self):
44 | return self.__class__.__name__
45 |
46 |
47 | # noinspection PyUnresolvedReferences,PyShadowingNames
48 | class Shape(AbstractShape):
49 | """General shape object.
50 |
51 | Examples
52 | --------
53 |
54 | Cartesian coordinates
55 |
56 | >>> points = np.random.uniform(-0.5, 2.5, size=(500, 2))
57 | >>> s = lp.Shape((2, 2))
58 | >>> s.limits()
59 | [[0. 2. ]
60 | [0. 2.]]
61 |
62 | >>> import matplotlib.pyplot as plt
63 | >>> mask = s.contains(points)
64 | >>> s.plot(plt.gca())
65 | >>> plt.scatter(*points[mask].T, s=3, color="g")
66 | >>> plt.scatter(*points[~mask].T, s=3, color="r")
67 | >>> plt.gca().set_aspect("equal")
68 | >>> plt.show()
69 |
70 | Angled coordinate system
71 |
72 | >>> s = lp.Shape((2, 2), basis=[[1, 0.2], [0, 1]])
73 | >>> s.limits()
74 | [[0. 2. ]
75 | [0. 2.4]]
76 |
77 | >>> mask = s.contains(points)
78 | >>> s.plot(plt.gca())
79 | >>> plt.scatter(*points[mask].T, s=3, color="g")
80 | >>> plt.scatter(*points[~mask].T, s=3, color="r")
81 | >>> plt.gca().set_aspect("equal")
82 | >>> plt.show()
83 | """
84 |
85 | def __init__(self, shape, pos=None, basis=None):
86 | if not hasattr(shape, "__len__"):
87 | shape = [shape]
88 | super().__init__(len(shape), pos)
89 | self.size = np.array(shape)
90 | self.basis = None if basis is None else np.array(basis)
91 |
92 | def _build(self):
93 | corners = list(itertools.product(*zip(np.zeros(self.dim), self.size)))
94 | corners = self.pos + np.array(corners)
95 | edges = None
96 | surfs = None
97 | if self.dim == 2:
98 | edges = np.array([[0, 1], [0, 2], [2, 3], [3, 1]])
99 | surfs = np.array([[0, 1, 3, 2, 0]])
100 | elif self.dim == 3:
101 | # Edge indices
102 | edges = np.array(
103 | [
104 | [0, 2],
105 | [2, 3],
106 | [3, 1],
107 | [1, 0],
108 | [4, 6],
109 | [6, 7],
110 | [7, 5],
111 | [5, 4],
112 | [0, 4],
113 | [2, 6],
114 | [3, 7],
115 | [1, 5],
116 | ]
117 | )
118 | # Surface indices
119 | surfs = np.array(
120 | [
121 | [0, 2, 3, 1],
122 | [4, 6, 7, 5],
123 | [0, 4, 6, 2],
124 | [2, 6, 7, 3],
125 | [3, 7, 5, 1],
126 | [1, 5, 4, 0],
127 | ]
128 | )
129 | if self.basis is not None:
130 | corners = np.inner(corners, self.basis.T)
131 | return corners, edges, surfs
132 |
133 | def limits(self):
134 | corners, _, _ = self._build()
135 | lims = np.array([np.min(corners, axis=0), np.max(corners, axis=0)])
136 | return lims.T
137 |
138 | def contains(self, points, tol=0.0):
139 | if self.basis is not None:
140 | points = np.inner(points, np.linalg.inv(self.basis.T))
141 | mask = np.logical_and(
142 | self.pos - tol <= points, points <= self.pos + self.size + tol
143 | )
144 | return np.all(mask, axis=1)
145 |
146 | def plot(self, ax, color="k", lw=0.0, alpha=0.2, **kwargs): # pragma: no cover
147 | corners, edges, surfs = self._build()
148 | segments = corners[edges]
149 | lines = draw_lines(ax, segments, color=color, lw=lw)
150 | segments = corners[surfs]
151 | if self.dim < 3:
152 | surfaces = ax.fill(*segments.T, color=color, alpha=alpha)
153 | elif self.dim == 3:
154 | surfaces = draw_surfaces(ax, segments, color=color, alpha=alpha)
155 | else:
156 | raise NotImplementedError("Can't plot shape in D>3!")
157 | return lines, surfaces
158 |
159 |
160 | # noinspection PyUnresolvedReferences,PyShadowingNames
161 | class Circle(AbstractShape):
162 | """Circle shape.
163 |
164 | Examples
165 | --------
166 |
167 | >>> s = lp.Circle((0, 0), radius=2)
168 | >>> s.limits()
169 | [[-2. 2.]
170 | [-2. 2.]]
171 |
172 | >>> import matplotlib.pyplot as plt
173 | >>> points = np.random.uniform(-2, 2, size=(500, 2))
174 | >>> mask = s.contains(points)
175 | >>> s.plot(plt.gca())
176 | >>> plt.scatter(*points[mask].T, s=3, color="g")
177 | >>> plt.scatter(*points[~mask].T, s=3, color="r")
178 | >>> plt.gca().set_aspect("equal")
179 | >>> plt.show()
180 |
181 | """
182 |
183 | def __init__(self, pos, radius):
184 | super().__init__(len(pos), pos)
185 | self.radius = radius
186 |
187 | def limits(self):
188 | rad = np.full(self.dim, self.radius)
189 | lims = self.pos + np.array([-rad, +rad])
190 | return lims.T
191 |
192 | def contains(self, points, tol=0.0):
193 | dists = np.sqrt(np.sum(np.square(points - self.pos), axis=1))
194 | return dists <= self.radius + tol
195 |
196 | def plot(self, ax, color="k", lw=0.0, alpha=0.2, **kwargs): # pragma: no cover
197 | xy = tuple(self.pos)
198 | line = plt.Circle(xy, self.radius, lw=lw, color=color, fill=False)
199 | ax.add_artist(line)
200 | surf = plt.Circle(xy, self.radius, lw=0, color=color, alpha=alpha, fill=True)
201 | ax.add_artist(surf)
202 | return line, surf
203 |
204 |
205 | # noinspection PyUnresolvedReferences,PyShadowingNames
206 | class Donut(AbstractShape):
207 | """Circle shape with cut-out in the middle.
208 |
209 | Examples
210 | --------
211 |
212 | >>> s = lp.Donut((0, 0), radius_outer=2, radius_inner=1)
213 | >>> s.limits()
214 | [[-2. 2.]
215 | [-2. 2.]]
216 |
217 | >>> import matplotlib.pyplot as plt
218 | >>> points = np.random.uniform(-2, 2, size=(500, 2))
219 | >>> mask = s.contains(points)
220 | >>> s.plot(plt.gca())
221 | >>> plt.scatter(*points[mask].T, s=3, color="g")
222 | >>> plt.scatter(*points[~mask].T, s=3, color="r")
223 | >>> plt.gca().set_aspect("equal")
224 | >>> plt.show()
225 |
226 | """
227 |
228 | def __init__(self, pos, radius_outer, radius_inner):
229 | super().__init__(len(pos), pos)
230 | self.radii = np.array([radius_inner, radius_outer])
231 |
232 | def limits(self):
233 | rad = np.full(self.dim, self.radii[1])
234 | lims = self.pos + np.array([-rad, +rad])
235 | return lims.T
236 |
237 | def contains(self, points, tol=1e-10):
238 | dists = np.sqrt(np.sum(np.square(points - self.pos), axis=1))
239 | return np.logical_and(
240 | self.radii[0] - tol <= dists, dists <= self.radii[1] + tol
241 | )
242 |
243 | def plot(self, ax, color="k", lw=0.0, alpha=0.2, **kwargs): # pragma: no cover
244 | n = 100
245 |
246 | theta = np.linspace(0, 2 * np.pi, n, endpoint=True)
247 | xs = np.outer(self.radii, np.cos(theta))
248 | ys = np.outer(self.radii, np.sin(theta))
249 | # in order to have a closed area, the circles
250 | # should be traversed in opposite directions
251 | xs[1, :] = xs[1, ::-1]
252 | ys[1, :] = ys[1, ::-1]
253 |
254 | line1 = ax.plot(xs[0], ys[0], color=color, lw=lw)[0]
255 | line2 = ax.plot(xs[1], ys[1], color=color, lw=lw)[0]
256 | surf = ax.fill(np.ravel(xs), np.ravel(ys), fc=color, alpha=alpha, ec=None)
257 |
258 | return [line1, line2], surf
259 |
260 |
261 | # noinspection PyUnresolvedReferences,PyShadowingNames
262 | class ConvexHull(AbstractShape):
263 | """Shape defined by convex hull of arbitrary points.
264 |
265 | Examples
266 | --------
267 |
268 | >>> s = lp.ConvexHull([[0, 0], [2, 0], [2, 1], [1, 2], [0, 2]])
269 | >>> s.limits()
270 | [[0. 2.]
271 | [0. 2.]]
272 |
273 | >>> import matplotlib.pyplot as plt
274 | >>> points = np.random.uniform(-0.5, 2.5, size=(500, 2))
275 | >>> mask = s.contains(points)
276 | >>> s.plot(plt.gca())
277 | >>> plt.scatter(*points[mask].T, s=3, color="g")
278 | >>> plt.scatter(*points[~mask].T, s=3, color="r")
279 | >>> plt.gca().set_aspect("equal")
280 | >>> plt.show()
281 |
282 | """
283 |
284 | def __init__(self, points):
285 | dim = len(points[0])
286 | super().__init__(dim)
287 | self.hull = scipy.spatial.ConvexHull(points)
288 |
289 | def limits(self):
290 | points = self.hull.points
291 | return np.array([np.min(points, axis=0), np.max(points, axis=0)]).T
292 |
293 | def contains(self, points, tol=1e-10):
294 | return np.all(
295 | np.add(
296 | np.dot(points, self.hull.equations[:, :-1].T),
297 | self.hull.equations[:, -1],
298 | )
299 | <= tol, # noqa: W503
300 | axis=1,
301 | )
302 |
303 | def plot(self, ax, color="k", lw=0.0, alpha=0.2, **kwargs): # pragma: no cover
304 | if self.dim == 2:
305 | segments = self.hull.points[self.hull.simplices]
306 | lines = draw_lines(ax, segments, color=color, lw=lw)
307 | # segments = self.hull.points[surf]
308 | segments = self.hull.points[self.hull.vertices]
309 | surfaces = ax.fill(*segments.T, fc=color, alpha=alpha, ec=None)
310 |
311 | elif self.dim == 3:
312 | segments = np.array(
313 | [self.hull.points[np.append(i, i[0])] for i in self.hull.simplices]
314 | )
315 | lines = draw_lines(ax, segments, color=color, lw=lw)
316 |
317 | surfaces = np.array([self.hull.points[i] for i in self.hull.simplices])
318 | draw_surfaces(ax, surfaces, color=color, alpha=alpha)
319 | else:
320 | raise NotImplementedError("Can't plot shape in D>3!")
321 |
322 | return lines, surfaces
323 |
--------------------------------------------------------------------------------
/lattpy/spatial.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | """Spatial algorithms and data structures."""
12 |
13 | import math
14 | import numpy as np
15 | import itertools
16 | import scipy.spatial
17 | import matplotlib.pyplot as plt
18 | from typing import Iterable, Sequence, Union
19 | from .utils import ArrayLike, min_dtype, chain
20 | from .plotting import draw_points, draw_vectors, draw_lines, draw_surfaces
21 |
22 |
23 | __all__ = [
24 | "distance",
25 | "distances",
26 | "interweave",
27 | "vindices",
28 | "vrange",
29 | "cell_size",
30 | "cell_volume",
31 | "compute_vectors",
32 | "KDTree",
33 | "VoronoiTree",
34 | "WignerSeitzCell",
35 | "rx",
36 | "ry",
37 | "rz",
38 | "rotate2d",
39 | "rotate3d",
40 | ]
41 |
42 |
43 | def distance(r1: np.ndarray, r2: np.ndarray, decimals: int = None) -> float:
44 | """Calculates the euclidian distance bewteen two points.
45 |
46 | Parameters
47 | ----------
48 | r1 : (D, ) np.ndarray
49 | First input point.
50 | r2 : (D, ) np.ndarray
51 | Second input point of matching size.
52 | decimals: int, optional
53 | Decimals for rounding the output.
54 |
55 | Returns
56 | -------
57 | distance: float
58 | The distance between the input points.
59 | """
60 | dist = math.sqrt(np.sum(np.square(r1 - r2)))
61 | if decimals is not None:
62 | dist = round(dist, decimals)
63 | return dist
64 |
65 |
66 | def distances(r1: ArrayLike, r2: ArrayLike, decimals: int = None) -> np.ndarray:
67 | """Calculates the euclidian distance between multiple points.
68 |
69 | Parameters
70 | ----------
71 | r1 : (N, D) array_like
72 | First input point.
73 | r2 : (N, D) array_like
74 | Second input point of matching size.
75 | decimals: int, optional
76 | Optional decimals to round distance to.
77 |
78 | Returns
79 | -------
80 | distances : (N, ) np.ndarray
81 | The distances for each pair of input points.
82 | """
83 | r1 = np.atleast_2d(r1)
84 | r2 = np.atleast_2d(r2)
85 | dist = np.sqrt(np.sum(np.square(r1 - r2), axis=1))
86 | if decimals is not None:
87 | dist = np.round(dist, decimals=decimals)
88 | return dist
89 |
90 |
91 | def interweave(arrays: Sequence[np.ndarray]) -> np.ndarray:
92 | """Interweaves multiple arrays along the first axis
93 |
94 | Example
95 | -------
96 | >>> arr1 = np.array([[1, 1], [3, 3], [5, 5]])
97 | >>> arr2 = np.array([[2, 2], [4, 4], [6, 6]])
98 | >>> interweave([arr1, arr2])
99 | array([[1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6]])
100 |
101 | Parameters
102 | ----------
103 | arrays: (M) Sequence of (N, ...) array_like
104 | The input arrays to interwave. The shape of all arrays must match.
105 |
106 | Returns
107 | -------
108 | interweaved: (M*N, ....) np.ndarray
109 | """
110 | shape = list(arrays[0].shape)
111 | shape[0] = sum(x.shape[0] for x in arrays)
112 | result = np.empty(shape, dtype=arrays[0].dtype)
113 | n = len(arrays)
114 | for i, arr in enumerate(arrays):
115 | result[i::n] = arr
116 | return result
117 |
118 |
119 | def vindices(
120 | limits: Iterable[Sequence[int]],
121 | sort_axis: int = 0,
122 | dtype: Union[int, str, np.dtype] = None,
123 | ) -> np.ndarray:
124 | """Return an array representing the indices of a d-dimensional grid.
125 |
126 | Parameters
127 | ----------
128 | limits: (D, 2) array_like
129 | The limits of the indices for each axis.
130 | sort_axis: int, optional
131 | Optional axis that is used to sort indices.
132 | dtype: int or str or np.dtype, optional
133 | Optional data-type for storing the lattice indices. By default the given limits
134 | are checked to determine the smallest possible data-type.
135 |
136 | Returns
137 | -------
138 | vectors: (N, D) np.ndarray
139 | """
140 | if dtype is None:
141 | dtype = min_dtype(limits, signed=True)
142 | limits = np.asarray(limits)
143 | dim = limits.shape[0]
144 |
145 | # Create meshgrid reshape grid to array of indices
146 |
147 | # version 1:
148 | # axis = np.meshgrid(*(np.arange(*lim, dtype=dtype) for lim in limits))
149 | # nvecs = np.asarray([np.asarray(a).flatten("F") for a in axis]).T
150 |
151 | # version 2:
152 | # slices = [slice(lim[0], lim[1], 1) for lim in limits]
153 | # nvecs = np.mgrid[slices].astype(dtype).reshape(dim, -1).T
154 |
155 | # version 3:
156 | size = limits[:, 1] - limits[:, 0]
157 | nvecs = np.indices(size, dtype=dtype).reshape(dim, -1).T + limits[:, 0]
158 |
159 | # Optionally sort indices along given axis
160 | if sort_axis is not None:
161 | nvecs = nvecs[np.lexsort(nvecs.T[[sort_axis]])]
162 |
163 | return nvecs
164 |
165 |
166 | # noinspection PyIncorrectDocstring
167 | def vrange(
168 | start=None,
169 | *args,
170 | dtype: Union[int, str, np.dtype] = None,
171 | sort_axis: int = 0,
172 | **kwargs,
173 | ) -> np.ndarray:
174 | """Return evenly spaced vectors within a given interval.
175 |
176 | Parameters
177 | ----------
178 | start: array_like, optional
179 | The starting value of the interval. The interval includes this value.
180 | The default start value is 0.
181 | stop: array_like
182 | The end value of the interval.
183 | step: array_like, optional
184 | Spacing between values. If `start` and `stop` are sequences and the `step`
185 | is a scalar the given step size is used for all dimensions of the vectors.
186 | The default step size is 1.
187 | sort_axis: int, optional
188 | Optional axis that is used to sort indices.
189 | dtype: dtype, optional
190 | The type of the output array. If `dtype` is not given, infer the data
191 | type from the other input arguments.
192 |
193 | Returns
194 | -------
195 | vectors: (N, D) np.ndarray
196 | """
197 | # parse arguments
198 | if len(args) == 0:
199 | stop = start
200 | start = np.zeros_like(stop)
201 | step = kwargs.get("step", 1.0)
202 | elif len(args) == 1:
203 | stop = args[0]
204 | step = kwargs.get("step", 1.0)
205 | else:
206 | stop, step = args
207 |
208 | start = np.atleast_1d(start)
209 | stop = np.atleast_1d(stop)
210 | if step is None:
211 | step = np.ones_like(start)
212 | elif not hasattr(step, "__len__"):
213 | step = np.ones_like(start) * step
214 |
215 | # Create grid and reshape to array of vectors
216 | slices = [slice(i, f, s) for i, f, s in zip(start, stop, step)]
217 | array = np.mgrid[slices].reshape(len(slices), -1).T
218 | # Optionally sort array along given axis
219 | if sort_axis is not None:
220 | array = array[np.lexsort(array.T[[sort_axis]])]
221 |
222 | return array if dtype is None else array.astype(dtype)
223 |
224 |
225 | def cell_size(vectors: ArrayLike) -> np.ndarray:
226 | """Computes the shape of the box spawned by the given vectors.
227 |
228 | Parameters
229 | ----------
230 | vectors: array_like
231 | The basis vectors defining the cell.
232 |
233 | Returns
234 | -------
235 | size: np.ndarray
236 | """
237 | max_values = np.max(vectors, axis=0)
238 | min_values = np.min(vectors, axis=0)
239 | min_values[min_values > 0] = 0
240 | return max_values - min_values
241 |
242 |
243 | def cell_volume(vectors: ArrayLike) -> float:
244 | r"""Computes the volume of the unit cell defined by the primitive vectors.
245 |
246 | The volume of the unit-cell in two and three dimensions is defined by
247 |
248 | .. math::
249 | V_{2d} = \abs{a_1 \cross a_2}, \quad V_{3d} = a_1 \cdot \abs{a_2 \cross a_3}
250 |
251 | For higher dimensions the volume is computed using the determinant:
252 |
253 | .. math::
254 | V_{d} = \sqrt{\det{A A^T}}
255 |
256 | where :math:`A` is the array of vectors.
257 |
258 | Parameters
259 | ----------
260 | vectors: array_like
261 | The basis vectors defining the cell.
262 |
263 | Returns
264 | -------
265 | vol: float
266 | The volume of the unit cell.
267 | """
268 | dim = len(vectors)
269 | if dim == 1:
270 | v = float(vectors)
271 | elif dim == 2:
272 | v = np.cross(vectors[0], vectors[1])
273 | elif dim == 3:
274 | cross = np.cross(vectors[1], vectors[2])
275 | v = np.dot(vectors[0], cross)
276 | else:
277 | v = np.sqrt(np.linalg.det(np.dot(vectors.T, vectors)))
278 | return abs(v)
279 |
280 |
281 | def compute_vectors(
282 | a: float,
283 | b: float = None,
284 | c: float = None,
285 | alpha: float = None,
286 | beta: float = None,
287 | gamma: float = None,
288 | decimals: int = 0,
289 | ) -> np.ndarray:
290 | """Computes lattice vectors by the lengths and angles."""
291 | if b is None and c is None:
292 | vectors = [a]
293 | elif c is None:
294 | alpha = np.deg2rad(alpha)
295 | ax = a
296 | bx = b * np.cos(alpha)
297 | by = b * np.sin(alpha)
298 | vectors = np.array([[ax, 0], [bx, by]])
299 | else:
300 | alpha = np.deg2rad(alpha)
301 | beta = np.deg2rad(beta)
302 | gamma = np.deg2rad(gamma)
303 | ax = a
304 | bx = b * np.cos(gamma)
305 | by = b * np.sin(gamma)
306 | cx = c * np.cos(beta)
307 | cy = (abs(c) * abs(b) * np.cos(alpha) - bx * cx) / by
308 | cz = np.sqrt(c**2 - cx**2 - cy**2)
309 | vectors = np.array([[ax, 0, 0], [bx, by, 0], [cx, cy, cz]])
310 | if decimals:
311 | vectors = np.round(vectors, decimals=decimals)
312 | return vectors
313 |
314 |
315 | # noinspection PyUnresolvedReferences
316 | class KDTree(scipy.spatial.cKDTree):
317 | """Simple wrapper of scipy's cKTree with global query settings."""
318 |
319 | def __init__(self, points, k=1, max_dist=np.inf, eps=0.0, p=2, boxsize=None):
320 | super().__init__(points, boxsize=boxsize)
321 | self.max_dist = max_dist
322 | self.k = k
323 | self.p = p
324 | self.eps = eps
325 |
326 | def __repr__(self):
327 | return f"{self.__class__.__name__}(k: {self.k}, p: {self.p}, eps: {self.eps})"
328 |
329 | def query_ball_point(self, x, r, *_):
330 | return super().query_ball_point(x, r, self.p, self.eps)
331 |
332 | def query_ball_tree(self, other: Union["KDTree", scipy.spatial.KDTree], r, *_):
333 | return super().query_ball_tree(other, r, self.p, self.eps)
334 |
335 | def query_pairs(self, r, *_):
336 | return super().query_pairs(r, self.p, self.eps)
337 |
338 | def query(
339 | self, x=None, num_jobs=1, decimals=None, include_zero=False, compact=True, *_
340 | ):
341 | x = self.data if x is None else x
342 | dists, neighbors = super().query(
343 | x, self.k, self.eps, self.p, self.max_dist, num_jobs
344 | )
345 |
346 | # Remove zero-distance neighbors and convert dtype
347 | if not include_zero and np.all(dists[:, 0] == 0):
348 | dists = dists[:, 1:]
349 | neighbors = neighbors[:, 1:]
350 | neighbors = neighbors.astype(min_dtype(self.n, signed=False))
351 |
352 | # Remove neighbors with distance larger than max_dist
353 | if self.max_dist < np.inf:
354 | invalid = dists > self.max_dist
355 | neighbors[invalid] = self.n
356 | dists[invalid] = np.inf
357 |
358 | # Remove all invalid columns
359 | if compact:
360 | mask = np.any(dists != np.inf, axis=0)
361 | neighbors = neighbors[:, mask]
362 | dists = dists[:, mask]
363 |
364 | # Round distances
365 | if decimals is not None:
366 | dists = np.round(dists, decimals=decimals)
367 |
368 | return neighbors, dists
369 |
370 |
371 | class VoronoiTree:
372 | def __init__(self, points):
373 | points = np.asarray(points)
374 | dim = points.shape[1]
375 | edges = list()
376 | if dim == 1:
377 | vertices = points / 2
378 | idx = np.where((vertices == np.zeros(vertices.shape[1])).all(axis=1))[0]
379 | vertices = np.delete(vertices, idx)
380 | vertices = np.atleast_2d(vertices).T
381 | else:
382 | vor = scipy.spatial.Voronoi(points)
383 | # Save only finite vertices
384 | vertices = vor.vertices # noqa
385 | for pointidx, simplex in zip(vor.ridge_points, vor.ridge_vertices): # noqa
386 | simplex = np.asarray(simplex)
387 | if np.all(simplex >= 0):
388 | edges.append(simplex)
389 |
390 | self.dim = dim
391 | self.points = points
392 | self.edges = edges
393 | self.vertices = vertices
394 | self.tree = scipy.spatial.KDTree(points)
395 | self.origin = self.query(np.zeros(dim))
396 |
397 | def query(self, x, k=1, eps=0):
398 | return self.tree.query(x, k, eps) # noqa
399 |
400 | def draw(
401 | self,
402 | ax=None,
403 | color="C0",
404 | size=3,
405 | lw=1,
406 | alpha=0.15,
407 | point_color="k",
408 | point_size=3,
409 | draw_data=True,
410 | points=True,
411 | draw=True,
412 | fill=True,
413 | ): # pragma: no cover
414 | if ax is None:
415 | fig = plt.figure()
416 | ax = fig.add_subplot(111, projection="3d" if self.dim == 3 else None)
417 |
418 | if draw_data:
419 | draw_points(ax, self.points, size=point_size, color=point_color)
420 | if self.dim > 1:
421 | draw_vectors(ax, self.points, lw=0.5, color=point_color)
422 |
423 | if points:
424 | draw_points(ax, self.vertices, size=size, color=color)
425 |
426 | if self.dim == 2 and draw:
427 | segments = np.array([self.vertices[i] for i in self.edges])
428 | draw_lines(ax, segments, color=color, lw=lw)
429 | elif self.dim == 3:
430 | if draw:
431 | segments = np.array(
432 | [self.vertices[np.append(i, i[0])] for i in self.edges]
433 | )
434 | draw_lines(ax, segments, color=color, lw=lw)
435 | if fill:
436 | surfaces = np.array([self.vertices[i] for i in self.edges])
437 | draw_surfaces(ax, surfaces, color=color, alpha=alpha)
438 |
439 | if self.dim == 3:
440 | ax.set_aspect("equal")
441 | else:
442 | ax.set_aspect("equal", "box")
443 |
444 | return ax
445 |
446 | def __repr__(self):
447 | return f"{self.__class__.__name__}(vertices: {len(self.vertices)})"
448 |
449 | def __str__(self):
450 | return f"vertices:\n{self.vertices}\n" f"egdes:\n{self.edges}"
451 |
452 |
453 | class WignerSeitzCell(VoronoiTree):
454 | def __init__(self, points):
455 | super().__init__(points)
456 | self._root = self.query(np.zeros(self.dim))[1]
457 |
458 | @property
459 | def limits(self):
460 | return np.array(
461 | [np.min(self.vertices, axis=0), np.max(self.vertices, axis=0)]
462 | ).T
463 |
464 | @property
465 | def size(self):
466 | return self.limits[1] - self.limits[0]
467 |
468 | def check(self, points):
469 | cells = np.asarray(self.query(points)[1])
470 | return cells == self._root
471 |
472 | def arange(self, steps, offset=0.0):
473 | limits = self.limits * (1 + offset)
474 | steps = [steps] * self.dim if not hasattr(steps, "__len__") else steps
475 | return [np.arange(*lims, step=step) for lims, step in zip(limits, steps)]
476 |
477 | def linspace(self, nums, offset=0.0, endpoint=False):
478 | limits = self.limits * (1 + offset)
479 | nums = [nums] * self.dim if not hasattr(nums, "__len__") else nums
480 | values = list()
481 | for lims, num in zip(limits, nums):
482 | values.append(np.linspace(*lims, num=num, endpoint=endpoint))
483 | return values
484 |
485 | def meshgrid(self, nums=None, steps=None, offset=0.0, check=True, endpoint=False):
486 | if nums is not None:
487 | grid = np.array(np.meshgrid(*self.linspace(nums, offset, endpoint)))
488 | elif steps is not None:
489 | grid = np.array(np.meshgrid(*self.arange(steps, offset)))
490 | else:
491 | raise ValueError(
492 | "Either the number of points or the step size must be specified"
493 | )
494 |
495 | if check:
496 | lengths = grid.shape[1:]
497 | dims = range(len(lengths))
498 | for item in itertools.product(*[range(n) for n in lengths]):
499 | point = np.array([grid[d][item] for d in dims])
500 | if not self.check(point):
501 | for d in dims:
502 | grid[d][item] = np.nan
503 | return grid
504 |
505 | def symmetry_points(self):
506 | origin = np.zeros(self.dim)
507 | corners = self.vertices.copy()
508 | face_centers = None
509 | if self.dim == 1:
510 | return origin, corners, None, None
511 | elif self.dim == 2:
512 | edge_centers = np.zeros((len(self.edges), 2))
513 | for i, simplex in enumerate(self.edges):
514 | p1, p2 = self.vertices[simplex]
515 | edge_centers[i] = p1 + (p2 - p1) / 2
516 | elif self.dim == 3:
517 | edge_centers = list()
518 | face_centers = list()
519 | for i, simplex in enumerate(self.edges):
520 | edges = self.vertices[simplex]
521 | # compute face centers
522 | face_centers.append(np.mean(edges, axis=0))
523 | # compute edge centers
524 | for p1, p2 in chain(edges, cycle=True):
525 | edge_centers.append(p1 + (p2 - p1) / 2)
526 | edge_centers = np.asarray(edge_centers)
527 | face_centers = np.asarray(face_centers)
528 | else:
529 | raise NotImplementedError()
530 | return origin, corners, edge_centers, face_centers
531 |
532 |
533 | def rx(theta: float) -> np.ndarray:
534 | """X-Rotation matrix."""
535 | sin, cos = np.sin(theta), np.cos(theta)
536 | return np.array([[1, 0, 0], [0, cos, -sin], [0, sin, cos]])
537 |
538 |
539 | def ry(theta: float) -> np.ndarray:
540 | """Y-Rotation matrix."""
541 | sin, cos = np.sin(theta), np.cos(theta)
542 | return np.array([[cos, 0, sin], [0, 1, 0], [-sin, 0, +cos]])
543 |
544 |
545 | def rz(theta: float) -> np.ndarray:
546 | """Z-Rotation matrix."""
547 | sin, cos = np.sin(theta), np.cos(theta)
548 | return np.array([[cos, -sin, 0], [sin, cos, 0], [0, 0, 1]])
549 |
550 |
551 | def rot(
552 | thetax: float = 0.0, thetay: float = 0.0, thetaz: float = 0.0
553 | ): # pragma: no cover
554 | """General rotation matrix"""
555 | r = np.eye(3)
556 | if thetaz:
557 | r = np.dot(r, rz(thetaz))
558 | if thetay:
559 | r = np.dot(r, ry(thetay))
560 | if thetax:
561 | r = np.dot(r, rz(thetax))
562 | return r
563 |
564 |
565 | def rotate2d(a, theta, degree=True): # pragma: no cover
566 | """Applies the z-rotation matrix to a 2D point"""
567 | if degree:
568 | theta = np.deg2rad(theta)
569 | return np.dot(a, rz(theta)[:2, :2])
570 |
571 |
572 | def rotate3d(a, thetax=0.0, thetay=0.0, thetaz=0.0, degree=True): # pragma: no cover
573 | """Applies the general rotation matrix to a 3D point"""
574 | if degree:
575 | thetax = np.deg2rad(thetax)
576 | thetay = np.deg2rad(thetay)
577 | thetaz = np.deg2rad(thetaz)
578 | return np.dot(a, rot(thetax, thetay, thetaz))
579 |
--------------------------------------------------------------------------------
/lattpy/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | from hypothesis import settings
12 |
13 | settings.register_profile("lattpy", deadline=1000)
14 |
--------------------------------------------------------------------------------
/lattpy/tests/test_atom.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | from lattpy.atom import Atom
12 |
13 |
14 | def test_atom_uniqueness():
15 | atom1 = Atom("A")
16 | atom2 = Atom("A")
17 | assert atom1 == atom2
18 | assert atom1.__hash__() == atom2.__hash__()
19 |
20 | atom1 = Atom("A")
21 | atom2 = Atom("B")
22 | assert atom1 != atom2
23 | assert atom1.__hash__() != atom2.__hash__()
24 |
25 |
26 | def test_atom_params():
27 | atom = Atom("A", color="r", energy=1)
28 |
29 | assert atom.color == "r"
30 | assert atom["color"] == "r"
31 |
32 | assert atom.energy == 1
33 | assert atom["energy"] == 1
34 |
35 | atom["spin"] = 1
36 | assert atom.spin == 1
37 | assert atom["spin"] == 1
38 |
39 | atom.spin = 1
40 | assert atom.spin == 1
41 | assert atom["spin"] == 1
42 |
43 | assert atom.get("spin") == 1
44 |
45 | del atom["spin"]
46 |
47 | assert atom.get("spin", None) is None
48 |
49 |
50 | def test_atom_copy():
51 | atom = Atom("A", energy=1.0)
52 |
53 | copy = atom.copy()
54 | assert copy.name == atom.name
55 | assert copy.energy == atom.energy
56 | atom.energy = 2.0
57 | assert copy.energy != atom.energy
58 |
59 |
60 | def test_atom_to_dict():
61 | atom = Atom("A", energy=1.0)
62 | expected = {"name": "A", "color": None, "radius": 0.2, "energy": 1.0}
63 | actual = atom.dict()
64 | actual.pop("index")
65 | assert actual == expected
66 |
67 |
68 | def test_atom_param_length():
69 | atom = Atom("A", energy=1.0)
70 | assert len(atom) == 3
71 |
72 |
73 | def test_atom_iter():
74 | atom = Atom("A", energy=1.0)
75 | assert list(atom) == ["color", "radius", "energy"]
76 |
77 |
78 | def test_atoms_equal():
79 | atom1 = Atom("A", energy=1.0)
80 | atom2 = Atom("B")
81 | atom3 = Atom("B", energy=1.0)
82 | assert atom1 != atom2
83 | assert atom2 == atom3
84 | assert atom1 == "A"
85 | assert atom1 != "B"
86 | assert atom2 == "B"
87 | assert atom3 == "B"
88 |
--------------------------------------------------------------------------------
/lattpy/tests/test_basis.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | from pytest import mark
12 | import numpy as np
13 | from numpy.testing import assert_array_equal, assert_allclose
14 | from lattpy.basis import LatticeBasis
15 |
16 | PI = np.pi
17 | TWOPI = 2 * np.pi
18 |
19 | chain = LatticeBasis.chain(a=1.0)
20 | rchain = LatticeBasis(TWOPI)
21 |
22 | square = LatticeBasis.square(a=1.0)
23 | rsquare = LatticeBasis(TWOPI * np.eye(2))
24 |
25 | rect = LatticeBasis.rectangular(a1=2.0, a2=1.0)
26 | rrect = LatticeBasis(PI * np.array([[1, 0], [0, 2]]))
27 |
28 |
29 | hexagonal = LatticeBasis.hexagonal(a=1)
30 | rhexagonal = LatticeBasis(
31 | np.array([[+2.0943951, +3.62759873], [+2.0943951, -3.62759873]])
32 | )
33 | sc = LatticeBasis.sc(a=1.0)
34 | rsc = LatticeBasis(TWOPI * np.eye(3))
35 |
36 | fcc = LatticeBasis.fcc(a=1.0)
37 | rfcc = LatticeBasis(TWOPI * np.array([[+1, +1, -1], [+1, -1, +1], [-1, +1, +1]]))
38 | bcc = LatticeBasis.bcc(a=1.0)
39 | rbcc = LatticeBasis(TWOPI * np.array([[+1, +1, 0], [0, -1, +1], [-1, 0, +1]]))
40 |
41 | LATTICES = [chain, square, rect, hexagonal, sc, fcc, bcc]
42 | RLATTICES = [rchain, rsquare, rrect, rhexagonal, rsc, rfcc, rbcc]
43 |
44 |
45 | def assert_elements_equal1d(actual, expected):
46 | actual = np.unique(actual)
47 | expected = np.unique(expected)
48 | assert len(actual) == len(expected)
49 | return all(np.isin(actual, expected))
50 |
51 |
52 | def assert_allclose_elements(actual, expected, atol=0.0, rtol=1e-7):
53 | assert_allclose(np.sort(actual), np.sort(expected), rtol, atol)
54 |
55 |
56 | def assert_equal_elements(actual, expected):
57 | assert_array_equal(np.sort(actual), np.sort(expected))
58 |
59 |
60 | def test_is_reciprocal():
61 | for latt, rlatt in zip(LATTICES, RLATTICES):
62 | rvecs = rlatt.vectors
63 | assert latt.is_reciprocal(rvecs)
64 | assert not latt.is_reciprocal(-1 * rvecs)
65 | assert not latt.is_reciprocal(+2 * rvecs)
66 | assert not latt.is_reciprocal(0.5 * rvecs)
67 | assert not latt.is_reciprocal(0.0 * rvecs)
68 |
69 |
70 | def test_reciprocal_vectors():
71 | for latt, rlatt in zip(LATTICES, RLATTICES):
72 | expected = rlatt.vectors
73 | actual = latt.reciprocal_vectors()
74 | assert_allclose(expected, actual)
75 |
76 |
77 | def test_reciprocal_vectors_double():
78 | for latt in LATTICES:
79 | expected = latt.vectors
80 | actual = latt.reciprocal_lattice().reciprocal_vectors()
81 | assert_array_equal(expected, actual)
82 |
83 |
84 | def test_translate():
85 | # Square lattice
86 | expected = [2.0, 0.0]
87 | actual = square.translate([2, 0], [0.0, 0.0])
88 | assert_array_equal(expected, actual)
89 |
90 | expected = [0.0, 2.0]
91 | actual = square.translate([0, 2], [0.0, 0.0])
92 | assert_array_equal(expected, actual)
93 |
94 | expected = [1.0, 2.0]
95 | actual = square.translate([1, 2], [0.0, 0.0])
96 | assert_array_equal(expected, actual)
97 |
98 | # Rectangular lattice
99 | expected = [4.0, 0.0]
100 | actual = rect.translate([2, 0], [0.0, 0.0])
101 | assert_array_equal(expected, actual)
102 |
103 | expected = [0.0, 2.0]
104 | actual = rect.translate([0, 2], [0.0, 0.0])
105 | assert_array_equal(expected, actual)
106 |
107 | expected = [2.0, 2.0]
108 | actual = rect.translate([1, 2], [0.0, 0.0])
109 | assert_array_equal(expected, actual)
110 |
111 |
112 | def test_itranslate():
113 | # Square lattice
114 | expected = [2, 0], [0.0, 0.0]
115 | actual = square.itranslate([2.0, 0.0])
116 | assert_array_equal(expected, actual)
117 |
118 | expected = [0, 2], [0.0, 0.0]
119 | actual = square.itranslate([0.0, 2.0])
120 | assert_array_equal(expected, actual)
121 |
122 | expected = [1, 2], [0.0, 0.0]
123 | actual = square.itranslate([1.0, 2.0])
124 | assert_array_equal(expected, actual)
125 |
126 | # Rectangular lattice
127 | expected = [1, 0], [0.0, 0.0]
128 | actual = rect.itranslate([2.0, 0.0])
129 | assert_array_equal(expected, actual)
130 |
131 | expected = [0, 2], [0.0, 0.0]
132 | actual = rect.itranslate([0.0, 2.0])
133 | assert_array_equal(expected, actual)
134 |
135 | expected = [1, 1], [0.0, 0.0]
136 | actual = rect.itranslate([2.0, 1.0])
137 | assert_array_equal(expected, actual)
138 |
139 |
140 | def test_brillouin_zone():
141 | latt = LatticeBasis.square()
142 | bz = latt.brillouin_zone()
143 |
144 | expected = [[-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0], [1.0, 1.0]]
145 | assert_array_equal(bz.vertices / np.pi, expected)
146 |
147 | expected = [[0, 1], [0, 2], [1, 3], [2, 3]]
148 | assert_array_equal(bz.edges, expected)
149 |
150 |
151 | @mark.parametrize("shape", [(10,), (10, 20), (10, 20, 30)])
152 | def test_index_superindex_conversion(shape):
153 | shape = np.array(shape)
154 | latt = LatticeBasis.hypercubic(len(shape))
155 |
156 | num_cells = np.prod(shape)
157 | ind1 = np.arange(num_cells, dtype=np.int64)
158 | for i in ind1:
159 | index = latt.get_cell_index(i, shape)
160 | i2 = latt.get_cell_superindex(index, shape)
161 | assert i == i2
162 |
163 | indices = latt.get_cell_index(ind1, shape)
164 | ind2 = latt.get_cell_superindex(indices, shape)
165 | assert_allclose(ind2, ind1)
166 |
--------------------------------------------------------------------------------
/lattpy/tests/test_data.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | import numpy as np
12 | from numpy.testing import assert_array_equal
13 | from hypothesis import given, assume, strategies as st
14 | from lattpy import simple_square, simple_chain
15 |
16 |
17 | def _construct_lattices():
18 | latts = list()
19 |
20 | latt = simple_chain()
21 | latt.build(10)
22 | latts.append(latt)
23 |
24 | latt = simple_square()
25 | latt.build((5, 5))
26 | latts.append(latt)
27 |
28 | return latts
29 |
30 |
31 | LATTICES = _construct_lattices()
32 |
33 |
34 | @st.composite
35 | def lattices(draw):
36 | return draw(st.sampled_from(LATTICES))
37 |
38 |
39 | @given(lattices())
40 | def test_dim(latt):
41 | assert latt.dim == latt.data.dim
42 |
43 |
44 | @given(lattices())
45 | def test_dim(latt):
46 | assert len(latt.data.positions) == latt.data.num_sites
47 |
48 |
49 | def test_num_distances():
50 | chain, square = LATTICES
51 |
52 | assert chain.data.num_distances == 2
53 | assert square.data.num_distances == 2
54 |
55 |
56 | @given(lattices())
57 | def test_copy(latt):
58 | copy = latt.copy()
59 | copy.data.remove(0)
60 | assert copy.data.num_sites == latt.data.num_sites - 1
61 |
62 |
63 | @given(st.integers(0, 100))
64 | def test_remove(site):
65 | latt = simple_square()
66 | latt.build((10, 10))
67 | assume(1 < site < latt.num_sites - 1)
68 |
69 | # find neighbor site lower than the one to remove:
70 | i = 0
71 | for i in latt.nearest_neighbors(site):
72 | if i < site:
73 | break
74 | assume(i > 0)
75 | assume(site in list(latt.nearest_neighbors(i)))
76 |
77 | # Get the number of neighbors of the neighbor site of `site`.
78 | num_neighbors = len(latt.nearest_neighbors(i))
79 | # Delete the site
80 | latt.data.remove(site)
81 |
82 | # check that the number of neighbors is one less than before
83 | assert len(latt.nearest_neighbors(i)) == (num_neighbors - 1)
84 |
85 |
86 | def test_sort():
87 | latt = simple_square()
88 | latt.build((5, 5))
89 |
90 | assert np.max(np.diff(latt.data.indices, axis=0)[:, 0]) == 1
91 |
92 | latt.data.sort(ax=1)
93 | assert np.max(np.diff(latt.data.indices, axis=0)[:, 1]) == 1
94 |
95 |
96 | def test_append():
97 | latt = simple_square()
98 | latt.build((4, 4))
99 | num_sites_original = latt.num_sites
100 | latt2 = simple_square()
101 | latt2.build((5, 4), pos=(5, 0))
102 |
103 | latt.data.append(latt2.data)
104 |
105 | original_pos = latt2.positions.copy()
106 | pos = latt.positions[num_sites_original:]
107 | assert_array_equal(pos, original_pos)
108 |
109 |
110 | def test_datamap():
111 | # Chain
112 | latt = simple_chain()
113 | latt.build(5)
114 |
115 | dmap = latt.data.map()
116 | # fmt: off
117 | res = [
118 | True, False, True, False, False, True, False, False,
119 | True, False, False, True, False, False, True, False,
120 | ]
121 | # fmt: on
122 | assert dmap.size == len(res)
123 | assert_array_equal(dmap.onsite(0), res)
124 |
125 | # fmt: off
126 | res = [
127 | False, True, False, True, True, False, True, True,
128 | False, True, True, False, True, True, False, True,
129 | ]
130 | # fmt: on
131 | assert_array_equal(dmap.hopping(0), res)
132 |
133 |
134 | def test_dmap_zeros():
135 | # Chain
136 | latt = simple_chain()
137 | latt.build(5)
138 | dmap = latt.dmap()
139 |
140 | data = dmap.zeros()
141 | assert data.shape == (16,)
142 |
143 | data = dmap.zeros(norb=1)
144 | assert data.shape == (16, 1, 1)
145 |
146 | data = dmap.zeros(norb=2)
147 | assert data.shape == (16, 2, 2)
148 |
149 |
150 | def test_dmap_csr_hamiltonian():
151 | # Chain
152 | latt = simple_chain()
153 | latt.build(5)
154 |
155 | expected = 2 * np.eye(latt.num_sites)
156 | expected += np.eye(latt.num_sites, k=+1) + np.eye(latt.num_sites, k=-1)
157 |
158 | dmap = latt.dmap()
159 | data = np.zeros(dmap.size)
160 | data[dmap.onsite()] = 2
161 | data[dmap.hopping()] = 1
162 | result = dmap.build_csr(data).toarray()
163 |
164 | assert_array_equal(expected, result)
165 |
166 |
167 | def test_dmap_bsr_hamiltonian():
168 | # Chain
169 | latt = simple_chain()
170 | latt.build(5)
171 | norbs = 2
172 |
173 | size = norbs * latt.num_sites
174 | expected = 2 * np.eye(size)
175 | expected += np.eye(size, k=+norbs) + np.eye(size, k=-norbs)
176 |
177 | dmap = latt.dmap()
178 | data = np.zeros((dmap.size, norbs, norbs))
179 | data[dmap.onsite()] = 2 * np.eye(norbs)
180 | data[dmap.hopping()] = 1 * np.eye(norbs)
181 | result = dmap.build_bsr(data).toarray()
182 |
183 | assert_array_equal(expected, result)
184 |
185 |
186 | def test_site_mask():
187 | latt = simple_square()
188 | latt.build((4, 4))
189 | data = latt.data
190 |
191 | expect = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
192 | assert_array_equal(data.site_mask([1, 0]), expect)
193 |
194 | expect = [0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1]
195 | assert_array_equal(data.site_mask([0, 1]), expect)
196 |
197 |
198 | def test_find_sites():
199 | latt = simple_square()
200 | latt.build((4, 4))
201 | data = latt.data
202 | mins, maxs = [1, 1], [3, 3]
203 |
204 | expected = [6, 7, 8, 11, 12, 13, 16, 17, 18]
205 | assert_array_equal(data.find_sites(mins, maxs), expected)
206 |
207 | expected = [0, 1, 2, 3, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
208 | assert_array_equal(data.find_sites(mins, maxs, invert=True), expected)
209 |
210 |
211 | def test_find_outer_sites():
212 | latt = simple_square()
213 | latt.build((4, 4))
214 | data = latt.data
215 | offset = 1
216 |
217 | expected = [0, 1, 2, 3, 4, 20, 21, 22, 23, 24]
218 | assert_array_equal(data.find_outer_sites(0, offset), expected)
219 |
220 | expected = [0, 4, 5, 9, 10, 14, 15, 19, 20, 24]
221 | assert_array_equal(data.find_outer_sites(1, offset), expected)
222 |
--------------------------------------------------------------------------------
/lattpy/tests/test_shape.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | import math
12 | import numpy as np
13 | from pytest import mark
14 | from numpy.testing import assert_array_equal, assert_allclose
15 | from hypothesis import given, settings, assume, strategies as st
16 | import hypothesis.extra.numpy as hnp
17 | from lattpy import shape
18 |
19 |
20 | dim = st.shared(st.integers(1, 3), key="d")
21 |
22 |
23 | @given(
24 | hnp.arrays(np.float64, dim, elements=st.floats(0.1, 100)),
25 | hnp.arrays(np.float64, dim, elements=st.floats(-100, 100)),
26 | )
27 | def test_shape(size, pos):
28 | s = shape.Shape(size, pos)
29 |
30 | d = len(pos)
31 | limits = (pos + np.array([np.zeros(d), size])).T
32 | assert_array_equal(s.limits(), limits)
33 |
34 | pts = np.random.uniform(-np.min(limits) - 10, +np.max(limits) + 10, size=(100, d))
35 | mask = s.contains(pts, tol=0)
36 | for point, res in zip(pts, mask):
37 | expected = True
38 | for i in range(d):
39 | expected = expected and (limits[i, 0] <= point[i] <= limits[i, 1])
40 | assert res == expected
41 |
42 |
43 | @given(hnp.arrays(np.float64, dim, elements=st.floats(0.1, 100)))
44 | def test_shape_basis(size):
45 | basis = np.eye(len(size))
46 | basis[0, 0] = 2
47 | s = shape.Shape(size, basis=basis)
48 |
49 | d = len(size)
50 | size[0] = size[0] * 2
51 | limits = (np.array([np.zeros(d), size])).T
52 | assert_allclose(s.limits(), limits, atol=1e-10)
53 |
54 | pts = np.random.uniform(-np.min(limits) - 10, +np.max(limits) + 10, size=(100, d))
55 | mask = s.contains(pts)
56 | for point, res in zip(pts, mask):
57 | expected = True
58 | for i in range(d):
59 | expected = expected and (limits[i, 0] <= point[i] <= limits[i, 1])
60 | assert res == expected
61 |
62 |
63 | @given(hnp.arrays(np.float64, 2, elements=st.floats(-100, 100)), st.floats(1, 100))
64 | def test_circle(pos, radius):
65 | s = shape.Circle(pos, radius)
66 |
67 | limits = np.array([pos - radius, pos + radius]).T
68 | assert_array_equal(s.limits(), limits)
69 |
70 | pts = np.random.uniform(-np.min(limits) - 10, +np.max(limits) + 10, size=(100, 2))
71 | mask = s.contains(pts)
72 | for point, res in zip(pts, mask):
73 | diff = point - pos
74 | dist = math.sqrt(diff[0] * diff[0] + diff[1] * diff[1])
75 | assert res == (dist <= radius)
76 |
77 |
78 | @given(
79 | hnp.arrays(np.float64, 2, elements=st.floats(-100, 100)),
80 | st.floats(50, 100),
81 | st.floats(0, 49),
82 | )
83 | def test_donut(pos, outer, inner):
84 | s = shape.Donut(pos, outer, inner)
85 |
86 | limits = np.array([pos - outer, pos + outer]).T
87 | assert_array_equal(s.limits(), limits)
88 |
89 | pts = np.random.uniform(-np.min(limits) - 10, +np.max(limits) + 10, size=(100, 2))
90 | mask = s.contains(pts)
91 | for point, res in zip(pts, mask):
92 | diff = point - pos
93 | dist = math.sqrt(diff[0] * diff[0] + diff[1] * diff[1])
94 | assert res == (inner <= dist <= outer)
95 |
96 |
97 | def test_convex_hull_2d():
98 | points = np.array([[0, 0], [2, 0], [2, 1], [1, 1]])
99 | s = shape.ConvexHull(points)
100 |
101 | limits = np.array([[0, 2], [0, 1]])
102 | assert_array_equal(s.limits(), limits)
103 |
104 | pts = np.random.uniform(-np.min(limits) - 1, +np.max(limits) + 1, size=(100, 2))
105 | mask = s.contains(pts)
106 | for point, res in zip(pts, mask):
107 | x, y = point
108 | if x <= 1:
109 | # Left half: triangle
110 | expected = (0 <= x <= 1) and (0 <= y <= x)
111 | else:
112 | # Right: square
113 | expected = (1 <= x <= 2) and (0 <= y <= 1)
114 | assert res == expected
115 |
--------------------------------------------------------------------------------
/lattpy/tests/test_spatial.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | import math
12 | import numpy as np
13 | from pytest import mark
14 | from numpy.testing import assert_array_equal, assert_allclose
15 | from hypothesis import given, strategies as st
16 | import hypothesis.extra.numpy as hnp
17 | from lattpy import spatial, simple_chain, simple_square, simple_cubic
18 |
19 |
20 | finite_floats = st.floats(-1e6, +1e6, allow_nan=False, allow_infinity=False)
21 |
22 |
23 | @given(
24 | hnp.arrays(np.float64, 10, elements=finite_floats),
25 | hnp.arrays(np.float64, 10, elements=finite_floats),
26 | )
27 | def test_distance(a, b):
28 | expected = math.sqrt(np.sum(np.square(a - b)))
29 |
30 | res = spatial.distance(a, b)
31 | assert res == expected
32 |
33 | res = spatial.distance(a, b, decimals=3)
34 | assert res == round(expected, 3)
35 |
36 |
37 | @given(
38 | hnp.arrays(np.float64, (5, 10), elements=finite_floats),
39 | hnp.arrays(np.float64, (5, 10), elements=finite_floats),
40 | )
41 | def test_distances(a, b):
42 | results = spatial.distances(a, b)
43 | for i in range(len(results)):
44 | expected = math.sqrt(np.sum(np.square(a[i] - b[i])))
45 | assert results[i] == expected
46 |
47 | results = spatial.distances(a, b, 3)
48 | for i in range(len(results)):
49 | expected = math.sqrt(np.sum(np.square(a[i] - b[i])))
50 | assert results[i] == np.round(expected, 3)
51 |
52 |
53 | @mark.parametrize(
54 | "arrays, result",
55 | [
56 | (([1, 3], [2, 4]), [1, 2, 3, 4]),
57 | (([1, 4], [2, 5], [3, 6]), [1, 2, 3, 4, 5, 6]),
58 | (([[1, 1], [3, 3]], [[2, 2], [4, 4]]), [[1, 1], [2, 2], [3, 3], [4, 4]]),
59 | ],
60 | )
61 | def test_interweave(arrays, result):
62 | assert_array_equal(spatial.interweave(np.array(arrays)), result)
63 |
64 |
65 | @mark.parametrize(
66 | "limits, result",
67 | [
68 | (([0, 1],), [[0]]),
69 | (([0, 1], [0, 1]), [[0, 0]]),
70 | (([0, 2],), [[0], [1]]),
71 | (([0, 2], [0, 1]), [[0, 0], [1, 0]]),
72 | (([0, 2], [0, 2]), [[0, 0], [0, 1], [1, 0], [1, 1]]),
73 | (([0, 3], [0, 2]), [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]]),
74 | ],
75 | )
76 | def test_vindices(limits, result):
77 | assert_array_equal(spatial.vindices(limits), result)
78 |
79 |
80 | @mark.parametrize(
81 | "stop, result",
82 | [
83 | (1, [[0]]),
84 | (3, [[0], [1], [2]]),
85 | ((3, 2), [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]]),
86 | ],
87 | )
88 | def test_vrange_stop(stop, result):
89 | assert_array_equal(spatial.vrange(stop), result)
90 |
91 |
92 | @mark.parametrize(
93 | "start, stop, result",
94 | [
95 | (0, 1, [[0]]),
96 | (0, 3, [[0], [1], [2]]),
97 | (1, 3, [[1], [2]]),
98 | ((0, 0), (3, 2), [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]]),
99 | ((1, 0), (3, 2), [[1, 0], [1, 1], [2, 0], [2, 1]]),
100 | ],
101 | )
102 | def test_vrange_startstop(start, stop, result):
103 | assert_array_equal(spatial.vrange(start, stop), result)
104 |
105 |
106 | @mark.parametrize(
107 | "vecs, result",
108 | [
109 | ([[1, 0], [0, 1]], [1.0, 1.0]),
110 | ([[2, 0], [0, 1]], [2.0, 1.0]),
111 | ([[2, 0], [1, 1]], [2.0, 1.0]),
112 | ([[2, 0], [-1, 1]], [3.0, 1.0]),
113 | ([[2, 0, 0], [0, 2, 0], [0, 0, 2]], [2.0, 2.0, 2.0]),
114 | ],
115 | )
116 | def test_cell_size(vecs, result):
117 | assert_array_equal(spatial.cell_size(vecs), result)
118 |
119 |
120 | @mark.parametrize(
121 | "vecs, result",
122 | [
123 | ([[1, 0], [0, 1]], 1.0),
124 | ([[2, 0], [0, 1]], 2.0),
125 | ([[2, 0], [0, 2]], 4.0),
126 | ([[2, 0, 0], [0, 2, 0], [0, 0, 2]], 8.0),
127 | ],
128 | )
129 | def test_cell_colume(vecs, result):
130 | assert spatial.cell_volume(vecs) == result
131 |
132 |
133 | def test_wignerseitz_symmetry_points():
134 | # Test 1D
135 | latt = simple_chain()
136 | ws = latt.wigner_seitz_cell()
137 | origin, corners, edge_centers, face_centers = ws.symmetry_points()
138 | assert_array_equal(origin, [0.0])
139 | assert_array_equal(corners, [[-0.5], [0.5]])
140 | assert edge_centers is None
141 | assert face_centers is None
142 |
143 | # Test 2D
144 | latt = simple_square()
145 | ws = latt.wigner_seitz_cell()
146 | origin, corners, edge_centers, face_centers = ws.symmetry_points()
147 |
148 | assert_array_equal(origin, [0.0, 0.0])
149 | assert_array_equal(
150 | 2 * corners, [[-1.0, -1.0], [1.0, -1.0], [-1.0, 1.0], [1.0, 1.0]]
151 | )
152 | assert_array_equal(
153 | 2 * edge_centers, [[0.0, -1.0], [-1.0, 0.0], [1.0, 0.0], [0.0, 1.0]]
154 | )
155 | assert face_centers is None
156 |
157 | # Test 3D
158 | latt = simple_cubic()
159 | ws = latt.wigner_seitz_cell()
160 | origin, corners, edge_centers, face_centers = ws.symmetry_points()
161 |
162 | c = [
163 | [-0.5, -0.5, -0.5],
164 | [0.5, -0.5, -0.5],
165 | [-0.5, -0.5, 0.5],
166 | [0.5, -0.5, 0.5],
167 | [-0.5, 0.5, -0.5],
168 | [0.5, 0.5, -0.5],
169 | [0.5, 0.5, 0.5],
170 | [-0.5, 0.5, 0.5],
171 | ]
172 | e = [
173 | [-0.5, -0.5, 0.0],
174 | [0.0, -0.5, -0.5],
175 | [0.5, -0.5, 0.0],
176 | [0.0, -0.5, 0.5],
177 | [-0.5, 0.5, 0.0],
178 | [0.0, 0.5, 0.5],
179 | [0.5, 0.5, 0.0],
180 | [0.0, 0.5, -0.5],
181 | [0.5, 0.0, 0.5],
182 | [0.0, 0.5, 0.5],
183 | [-0.5, 0.0, 0.5],
184 | [0.0, -0.5, 0.5],
185 | [-0.5, -0.5, 0.0],
186 | [-0.5, 0.0, -0.5],
187 | [-0.5, 0.5, 0.0],
188 | [-0.5, 0.0, 0.5],
189 | [0.5, 0.0, -0.5],
190 | [0.0, 0.5, -0.5],
191 | [-0.5, 0.0, -0.5],
192 | [0.0, -0.5, -0.5],
193 | [0.5, -0.5, 0.0],
194 | [0.5, 0.0, -0.5],
195 | [0.5, 0.5, 0.0],
196 | [0.5, 0.0, 0.5],
197 | ]
198 |
199 | f = [
200 | [0.0, -0.5, 0.0],
201 | [0.0, 0.5, 0.0],
202 | [0.0, 0.0, 0.5],
203 | [-0.5, 0.0, 0.0],
204 | [0.0, 0.0, -0.5],
205 | [0.5, 0.0, 0.0],
206 | ]
207 |
208 | assert_array_equal(origin, [0.0, 0.0, 0.0])
209 | assert_array_equal(corners, c)
210 | assert_array_equal(edge_centers, e)
211 | assert_array_equal(face_centers, f)
212 |
213 |
214 | def test_wigner_seitz_arange():
215 | latt = simple_square()
216 | ws = latt.wigner_seitz_cell()
217 |
218 | x, y = ws.arange(0.1)
219 | assert_allclose(x, np.arange(-0.5, +0.5, 0.1))
220 | assert_allclose(y, np.arange(-0.5, +0.5, 0.1))
221 |
222 | x, y = ws.arange((0.1, 0.2))
223 | assert_allclose(x, np.arange(-0.5, +0.5, 0.1))
224 | assert_allclose(y, np.arange(-0.5, +0.5, 0.2))
225 |
226 |
227 | def test_wigner_seitz_linspace():
228 | latt = simple_square()
229 | ws = latt.wigner_seitz_cell()
230 |
231 | x, y = ws.linspace(100, endpoint=True)
232 | assert_allclose(x, np.linspace(-0.5, +0.5, 100))
233 | assert_allclose(y, np.linspace(-0.5, +0.5, 100))
234 |
235 | x, y = ws.linspace((100, 200), endpoint=True)
236 | assert_allclose(x, np.linspace(-0.5, +0.5, 100))
237 | assert_allclose(y, np.linspace(-0.5, +0.5, 200))
238 |
239 |
240 | def test_wigner_seitz_meshgrid():
241 | latt = simple_square()
242 | ws = latt.wigner_seitz_cell()
243 |
244 | num = 101
245 | actual = ws.meshgrid(num, endpoint=True)
246 | x = np.linspace(-0.5, +0.5, num)
247 | y = np.linspace(-0.5, +0.5, num)
248 | expected = np.array(np.meshgrid(x, y))
249 | assert_allclose(actual, expected)
250 |
251 | step = 0.1
252 | actual = ws.meshgrid(steps=step, endpoint=True)
253 | x = np.arange(-0.5, +0.5, step)
254 | y = np.arange(-0.5, +0.5, step)
255 | expected = np.array(np.meshgrid(x, y))
256 | assert_allclose(actual, expected)
257 |
258 |
259 | def test_compute_vectors():
260 | # Test square vectors
261 | vecs = spatial.compute_vectors(1.0, 1.0, alpha=90)
262 | assert_allclose(vecs, np.eye(2), atol=1e-16)
263 |
264 | # Text hexagonal vectors
265 | vecs = spatial.compute_vectors(1.0, 1.0, alpha=60)
266 | expected = np.array([[1, 0], [0.5, math.sqrt(3) / 2]])
267 | assert_allclose(vecs, expected, atol=1e-16)
268 |
269 | # Test cubic vectors
270 | vecs = spatial.compute_vectors(1.0, 1.0, 1.0, alpha=90, beta=90, gamma=90)
271 | assert_allclose(vecs, np.eye(3), atol=1e-16)
272 |
273 |
274 | def test_rx():
275 | expected = np.eye(3)
276 | assert_allclose(spatial.rx(0), expected)
277 |
278 | expected = [
279 | [1.0, 0.0, 0.0],
280 | [0.0, 0.70710678, -0.70710678],
281 | [0.0, 0.70710678, 0.70710678],
282 | ]
283 | assert_allclose(spatial.rx(np.pi / 4), expected)
284 |
285 | expected = [[1, 0, 0], [0, 0, -1], [0, 1, 0]]
286 | assert_allclose(spatial.rx(np.pi / 2), expected, atol=1e-10)
287 |
288 | expected = [[1, 0, 0], [0, -1, 0], [0, 0, -1]]
289 | assert_allclose(spatial.rx(np.pi), expected, atol=1e-10)
290 |
291 |
292 | def test_ry():
293 | expected = np.eye(3)
294 | assert_allclose(spatial.ry(0), expected)
295 |
296 | expected = [
297 | [0.70710678, 0.0, 0.70710678],
298 | [0.0, 1.0, 0.0],
299 | [-0.70710678, 0.0, 0.70710678],
300 | ]
301 | assert_allclose(spatial.ry(np.pi / 4), expected)
302 |
303 | expected = np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
304 | assert_allclose(spatial.ry(np.pi / 2), expected, atol=1e-10)
305 |
306 | expected = [[-1, 0, 0], [0, 1, 0], [0, 0, -1]]
307 | assert_allclose(spatial.ry(np.pi), expected, atol=1e-10)
308 |
309 |
310 | def test_rz():
311 | expected = np.eye(3)
312 | assert_allclose(spatial.rz(0), expected)
313 |
314 | expected = [
315 | [0.70710678, -0.70710678, 0.0],
316 | [0.70710678, 0.70710678, 0.0],
317 | [0.0, 0.0, 1.0],
318 | ]
319 | assert_allclose(spatial.rz(np.pi / 4), expected)
320 |
321 | expected = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
322 | assert_allclose(spatial.rz(np.pi / 2), expected, atol=1e-10)
323 |
324 | expected = [[-1, 0, 0], [0, -1, 0], [0, 0, 1]]
325 | assert_allclose(spatial.rz(np.pi), expected, atol=1e-10)
326 |
--------------------------------------------------------------------------------
/lattpy/tests/test_structure.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | import pytest
12 | import numpy as np
13 | from numpy.testing import assert_array_equal, assert_allclose
14 | from hypothesis import given, strategies as st
15 | import hypothesis.extra.numpy as hnp
16 | from lattpy.utils import SiteOccupiedError, NoAtomsError, NoConnectionsError
17 | from lattpy import Atom
18 | from lattpy.structure import LatticeStructure
19 |
20 | atom = Atom()
21 |
22 |
23 | @given(st.integers(1, 3))
24 | def test_add_atom(dim):
25 | latt = LatticeStructure(np.eye(dim))
26 |
27 | latt.add_atom()
28 | assert_array_equal(latt.atom_positions[0], np.zeros(dim))
29 |
30 | with pytest.raises(SiteOccupiedError):
31 | latt.add_atom()
32 |
33 | with pytest.raises(ValueError):
34 | latt.add_atom(np.zeros(4))
35 |
36 |
37 | def test_get_alpha():
38 | latt = LatticeStructure(np.eye(2))
39 | at1 = latt.add_atom([0.0, 0.0], atom="A")
40 | at2 = latt.add_atom([0.5, 0.5], atom="B")
41 |
42 | assert latt.get_alpha("A") == [0]
43 | assert latt.get_alpha(at1) == 0
44 |
45 | assert latt.get_alpha("B") == [1]
46 | assert latt.get_alpha(at2) == 1
47 |
48 | latt = LatticeStructure(np.eye(2))
49 | latt.add_atom([0.0, 0.0], atom="A")
50 | latt.add_atom([0.5, 0.5], atom="A")
51 | assert latt.get_alpha("A") == [0, 1]
52 |
53 |
54 | def test_get_atom():
55 | latt = LatticeStructure(np.eye(2))
56 | at1 = latt.add_atom([0.0, 0.0], atom="A")
57 | at2 = latt.add_atom([0.5, 0.5], atom="B")
58 |
59 | assert latt.get_atom("A") == at1
60 | assert latt.get_atom(0) == at1
61 |
62 | assert latt.get_atom("B") == at2
63 | assert latt.get_atom(1) == at2
64 |
65 |
66 | def test_add_connection():
67 | latt = LatticeStructure(np.eye(2))
68 | latt.add_atom([0.0, 0.0], atom="A")
69 | latt.add_atom([0.5, 0.5], atom="B")
70 |
71 | latt.add_connection("A", "A", 1)
72 | latt.add_connection("A", "B", 1)
73 | latt.analyze()
74 |
75 | # Assert neighbor atom index is right
76 | assert all(latt.get_neighbors(alpha=0, distidx=0)[:, -1] == 1)
77 | assert all(latt.get_neighbors(alpha=0, distidx=1)[:, -1] == 0)
78 | assert all(latt.get_neighbors(alpha=1, distidx=0)[:, -1] == 0)
79 |
80 |
81 | def test_analyze_exceptions():
82 | latt = LatticeStructure(np.eye(2))
83 | with pytest.raises(NoAtomsError):
84 | latt.analyze()
85 |
86 | latt.add_atom()
87 | with pytest.raises(NoConnectionsError):
88 | latt.analyze()
89 |
90 |
91 | @given(hnp.arrays(np.int64, 2, elements=st.integers(0, 10)), st.integers(0, 1))
92 | def test_get_position(nvec, alpha):
93 | latt = LatticeStructure(np.eye(2))
94 | latt.add_atom([0.0, 0.0], atom="A")
95 | latt.add_atom([0.5, 0.5], atom="B")
96 |
97 | pos = latt.translate(nvec, latt.atom_positions[alpha])
98 | assert_allclose(latt.get_position(nvec, alpha), pos)
99 |
100 |
101 | @given(hnp.arrays(np.int64, (10, 2), elements=st.integers(0, 10)), st.integers(0, 1))
102 | def test_get_positions(nvecs, alpha):
103 | latt = LatticeStructure(np.eye(2))
104 | latt.add_atom([0.0, 0.0], atom="A")
105 | latt.add_atom([0.5, 0.5], atom="B")
106 |
107 | indices = np.array([[*nvec, alpha] for nvec in nvecs])
108 | results = latt.get_positions(indices)
109 | for res, nvec in zip(results, nvecs):
110 | pos = latt.translate(nvec, latt.atom_positions[alpha])
111 | assert_allclose(res, pos)
112 |
113 |
114 | def test_estimate_index():
115 | # Square lattice
116 | square = LatticeStructure.square()
117 |
118 | expected = [2, 0]
119 | actual = square.estimate_index([2.0, 0.0])
120 | assert_array_equal(expected, actual)
121 |
122 | expected = [0, 2]
123 | actual = square.estimate_index([0.0, 2.0])
124 | assert_array_equal(expected, actual)
125 |
126 | expected = [1, 2]
127 | actual = square.estimate_index([1.0, 2.0])
128 | assert_array_equal(expected, actual)
129 |
130 | # Rectangular lattice
131 | rect = LatticeStructure.rectangular(a1=2.0, a2=1.0)
132 |
133 | expected = [1, 0]
134 | actual = rect.estimate_index([2.0, 0.0])
135 | assert_array_equal(expected, actual)
136 |
137 | expected = [0, 2]
138 | actual = rect.estimate_index([0.0, 2.0])
139 | assert_array_equal(expected, actual)
140 |
141 | expected = [1, 1]
142 | actual = rect.estimate_index([2.0, 1.0])
143 | assert_array_equal(expected, actual)
144 |
145 |
146 | def test_get_neighbors():
147 | # chain
148 | latt = LatticeStructure.chain()
149 | latt.add_atom()
150 | latt.add_connections(2)
151 | # Nearest neighbors
152 | expected = np.array([[1, 0], [-1, 0]])
153 | for idx in latt.get_neighbors(alpha=0, distidx=0):
154 | assert any((expected == idx).all(axis=1))
155 | # Next nearest neighbors
156 | expected = np.array([[-2, 0], [2, 0]])
157 | for idx in latt.get_neighbors(alpha=0, distidx=1):
158 | assert any((expected == idx).all(axis=1))
159 |
160 | # square
161 | latt = LatticeStructure.square()
162 | latt.add_atom()
163 | latt.add_connections(2)
164 | # Nearest neighbors
165 | expected = np.array([[1, 0, 0], [0, -1, 0], [0, 1, 0], [-1, 0, 0]])
166 | for idx in latt.get_neighbors(alpha=0, distidx=0):
167 | assert any((expected == idx).all(axis=1))
168 | # Next nearest neighbors
169 | expected = np.array([[1, -1, 0], [-1, -1, 0], [-1, 1, 0], [1, 1, 0]])
170 | for idx in latt.get_neighbors(alpha=0, distidx=1):
171 | assert any((expected == idx).all(axis=1))
172 |
173 |
174 | def test_get_neighbor_positions():
175 | # chain
176 | latt = LatticeStructure.chain()
177 | latt.add_atom()
178 | latt.add_connections(2)
179 | # Nearest neighbors
180 | expected = np.array([[1], [-1]])
181 | for idx in latt.get_neighbor_positions(alpha=0, distidx=0):
182 | assert any((expected == idx).all(axis=1))
183 | # Next nearest neighbors
184 | expected = np.array([[-2], [2]])
185 | for idx in latt.get_neighbor_positions(alpha=0, distidx=1):
186 | assert any((expected == idx).all(axis=1))
187 |
188 | # square
189 | latt = LatticeStructure.square()
190 | latt.add_atom()
191 | latt.add_connections(2)
192 | # Nearest neighbors
193 | expected = np.array([[1, 0], [0, -1], [0, 1], [-1, 0]])
194 | for idx in latt.get_neighbor_positions(alpha=0, distidx=0):
195 | assert any((expected == idx).all(axis=1))
196 | # Next nearest neighbors
197 | expected = np.array([[1, -1], [-1, -1], [-1, 1], [1, 1]])
198 | for idx in latt.get_neighbor_positions(alpha=0, distidx=1):
199 | assert any((expected == idx).all(axis=1))
200 |
201 |
202 | def test_get_neighbor_vectors():
203 | # chain
204 | latt = LatticeStructure.chain()
205 | latt.add_atom()
206 | latt.add_connections(2)
207 | # Nearest neighbors
208 | expected = np.array([[1], [-1]])
209 | for idx in latt.get_neighbor_vectors(alpha=0, distidx=0):
210 | assert any((expected == idx).all(axis=1))
211 | # Next nearest neighbors
212 | expected = np.array([[-2], [2]])
213 | for idx in latt.get_neighbor_vectors(alpha=0, distidx=1):
214 | assert any((expected == idx).all(axis=1))
215 |
216 | # square
217 | latt = LatticeStructure.square()
218 | latt.add_atom()
219 | latt.add_connections(2)
220 | # Nearest neighbors
221 | expected = np.array([[1, 0], [0, -1], [0, 1], [-1, 0]])
222 | for idx in latt.get_neighbor_vectors(alpha=0, distidx=0):
223 | assert any((expected == idx).all(axis=1))
224 | # Next nearest neighbors
225 | expected = np.array([[1, -1], [-1, -1], [-1, 1], [1, 1]])
226 | for idx in latt.get_neighbor_vectors(alpha=0, distidx=1):
227 | assert any((expected == idx).all(axis=1))
228 |
229 |
230 | def test_get_base_atom_dict():
231 | latt = LatticeStructure(np.eye(2))
232 | ata = latt.add_atom([0, 0], atom="A")
233 | atb = latt.add_atom([0.5, 0], atom="B")
234 | latt.add_atom([0.5, 0.5], atom="B")
235 | result = latt.get_base_atom_dict()
236 |
237 | assert len(result) == 2
238 | assert_array_equal(result[ata], [[0, 0]])
239 | assert_array_equal(result[atb], [[0.5, 0.0], [0.5, 0.5]])
240 |
241 |
242 | def test_quick_setup():
243 | latt = LatticeStructure(np.eye(2), atoms={(0.0, 0.0): "A"})
244 | assert latt.num_base == 1
245 | assert latt.atoms[0] == "A"
246 | assert_array_equal(latt.atom_positions[0], [0.0, 0.0])
247 |
248 | latt = LatticeStructure(np.eye(2), atoms={(0.0, 0.0): "A"}, cons=1)
249 | assert_array_equal(latt.num_neighbors, [4])
250 |
251 | latt = LatticeStructure(np.eye(2), atoms={(0.0, 0.0): "A"}, cons={("A", "A"): 1})
252 | assert_array_equal(latt.num_neighbors, [4])
253 |
254 |
255 | def test_hash():
256 | latt1 = LatticeStructure(np.eye(2))
257 | latt1.add_atom()
258 |
259 | latt2 = LatticeStructure(np.eye(2))
260 | latt2.add_atom()
261 |
262 | assert latt1.__hash__() != latt2.__hash__() # Since atom indices are different
263 |
--------------------------------------------------------------------------------
/lattpy/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | import numpy as np
12 | from pytest import mark
13 | from numpy.testing import assert_array_equal
14 | from lattpy import utils
15 |
16 |
17 | @mark.parametrize(
18 | "a, signed, result",
19 | [
20 | (+127, False, np.uint8),
21 | (-127, False, np.int8),
22 | (+127, True, np.int8),
23 | (+128, False, np.uint8),
24 | (-128, False, np.int8),
25 | (+128, True, np.int16),
26 | ([-128, 127], False, np.int8),
27 | ([-128, 128], False, np.int16),
28 | ([-129, 127], False, np.int16),
29 | ([+30, 127], False, np.uint8),
30 | ],
31 | )
32 | def test_min_dtype(a, signed, result):
33 | assert utils.min_dtype(a, signed) == result
34 |
35 |
36 | @mark.parametrize(
37 | "items, cycle, result",
38 | [
39 | ([0, 1, 2], False, [[0, 1], [1, 2]]),
40 | ([0, 1, 2], True, [[0, 1], [1, 2], [2, 0]]),
41 | (["0", "1", "2"], False, [["0", "1"], ["1", "2"]]),
42 | (["0", "1", "2"], True, [["0", "1"], ["1", "2"], ["2", "0"]]),
43 | ],
44 | )
45 | def test_chain(items, cycle, result):
46 | assert_array_equal(utils.chain(items, cycle), result)
47 |
--------------------------------------------------------------------------------
/lattpy/utils.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | #
3 | # This code is part of lattpy.
4 | #
5 | # Copyright (c) 2022, Dylan Jones
6 | #
7 | # This code is licensed under the MIT License. The copyright notice in the
8 | # LICENSE file in the root directory and this permission notice shall
9 | # be included in all copies or substantial portions of the Software.
10 |
11 | """Contains miscellaneous utility methods."""
12 |
13 | import logging
14 | from typing import Iterable, List, Sequence, Union, Tuple
15 | import numpy as np
16 |
17 | __all__ = [
18 | "ArrayLike",
19 | "logger",
20 | "LatticeError",
21 | "ConfigurationError",
22 | "SiteOccupiedError",
23 | "NoAtomsError",
24 | "NoConnectionsError",
25 | "NotAnalyzedError",
26 | "NotBuiltError",
27 | "min_dtype",
28 | "chain",
29 | "create_lookup_table",
30 | "frmt_num",
31 | "frmt_bytes",
32 | "frmt_time",
33 | ]
34 |
35 | # define type for numpy `array_like` types
36 | ArrayLike = Union[int, float, Iterable, np.ndarray]
37 |
38 |
39 | # Configure package logger
40 | logger = logging.getLogger("lattpy")
41 |
42 | _CH = logging.StreamHandler()
43 | _CH.setLevel(logging.DEBUG)
44 |
45 | _FRMT_STR = "[%(asctime)s] %(levelname)-8s - %(name)-15s - %(message)s"
46 | _FRMT = logging.Formatter(_FRMT_STR, datefmt="%H:%M:%S")
47 |
48 | _CH.setFormatter(_FRMT) # Add formatter to stream handler
49 | logger.addHandler(_CH) # Add stream handler to package logger
50 |
51 | logger.setLevel(logging.WARNING) # Set initial logging level
52 |
53 |
54 | class LatticeError(Exception):
55 | pass
56 |
57 |
58 | class ConfigurationError(LatticeError):
59 | @property
60 | def msg(self):
61 | return self.args[0]
62 |
63 | @property
64 | def hint(self):
65 | return self.args[1]
66 |
67 | def __str__(self):
68 | msg, hint = self.args
69 | if hint:
70 | msg += f" ({hint})"
71 | return msg
72 |
73 |
74 | class SiteOccupiedError(ConfigurationError):
75 | def __init__(self, atom, pos):
76 | super().__init__(
77 | f"Can't add {atom} to lattice, position {pos} already occupied!"
78 | )
79 |
80 |
81 | class NoAtomsError(ConfigurationError):
82 | def __init__(self):
83 | super().__init__(
84 | "lattice doesn't contain any atoms use 'add_atom' to add an 'Atom'-object"
85 | )
86 |
87 |
88 | class NotAnalyzedError(ConfigurationError):
89 | def __init__(self):
90 | msg = "lattice not analyzed"
91 | hint = (
92 | "call 'analyze' after adding atoms and connections or "
93 | "use the 'analyze' keyword of 'add_connection'"
94 | )
95 | super().__init__(msg, hint)
96 |
97 |
98 | class NoConnectionsError(ConfigurationError):
99 | def __init__(self):
100 | msg = "base neighbors not configured"
101 | hint = (
102 | "call 'add_connection' after adding atoms or "
103 | "use the 'neighbors' keyword of 'add_atom'"
104 | )
105 | super().__init__(msg, hint)
106 |
107 |
108 | class NotBuiltError(ConfigurationError):
109 | def __init__(self):
110 | msg = "lattice has not been built"
111 | hint = "use the 'build' method to construct a finite size lattice model"
112 | super().__init__(msg, hint)
113 |
114 |
115 | def create_lookup_table(
116 | array: ArrayLike, dtype: Union[str, np.dtype] = np.uint8
117 | ) -> Tuple[np.ndarray, np.ndarray]:
118 | """Converts the given array to an array of indices linked to the unique values.
119 |
120 | Parameters
121 | ----------
122 | array : array_like
123 | dtype : int or np.dtype, optional
124 | Optional data-type for storing the indices of the unique values.
125 | By default `np.uint8` is used, since it is assumed that the
126 | input-array has only a few unique values.
127 |
128 | Returns
129 | -------
130 | values : np.ndarray
131 | The unique values occuring in the input-array.
132 | indices : np.ndarray
133 | The corresponding indices in the same shape as the input-array.
134 | """
135 | values = np.sort(np.unique(array))
136 | indices = np.zeros_like(array, dtype=dtype)
137 | for i, x in enumerate(values):
138 | mask = array == x
139 | indices[mask] = i
140 | return values, indices
141 |
142 |
143 | def min_dtype(
144 | a: Union[int, float, np.ndarray, Iterable], signed: bool = True
145 | ) -> np.dtype:
146 | """Returns the minimum required dtype to store the given values.
147 |
148 | Parameters
149 | ----------
150 | a : array_like
151 | One or more values for determining the dtype.
152 | Should contain the maximal expected values.
153 | signed : bool, optional
154 | If `True` the dtype is forced to be signed. The default is `True`.
155 |
156 | Returns
157 | -------
158 | dtype : dtype
159 | The required dtype.
160 | """
161 | if signed:
162 | a = -np.max(np.abs(a)) - 1
163 | else:
164 | amin, amax = np.min(a), np.max(a)
165 | if amin < 0:
166 | a = -amax - 1 if abs(amin) <= amax else amin
167 | else:
168 | a = amax
169 | return np.dtype(np.min_scalar_type(a))
170 |
171 |
172 | def chain(items: Sequence, cycle: bool = False) -> List:
173 | """Creates a chain between items
174 |
175 | Parameters
176 | ----------
177 | items : Sequence
178 | items to join to chain
179 | cycle : bool, optional
180 | cycle to the start of the chain if True, default: False
181 |
182 | Returns
183 | -------
184 | chain: list
185 | chain of items
186 |
187 | Example
188 | -------
189 | >>> print(chain(["x", "y", "z"]))
190 | [['x', 'y'], ['y', 'z']]
191 |
192 | >>> print(chain(["x", "y", "z"], True))
193 | [['x', 'y'], ['y', 'z'], ['z', 'x']]
194 | """
195 | result = list()
196 | for i in range(len(items) - 1):
197 | result.append([items[i], items[i + 1]])
198 | if cycle:
199 | result.append([items[-1], items[0]])
200 | return result
201 |
202 |
203 | def frmt_num(num: float, dec: int = 1, unit: str = "", div: float = 1000.0) -> str:
204 | """Returns a formatted string of a number.
205 |
206 | Parameters
207 | ----------
208 | num : float
209 | The number to format.
210 | dec : int, optional
211 | Number of decimals. The default is 1.
212 | unit : str, optional
213 | Optional unit suffix. By default no unit-strinmg is used.
214 | div : float, optional
215 | The divider used for units. The default is 1000.
216 |
217 | Returns
218 | -------
219 | num_str: str
220 | """
221 | for prefix in ["", "k", "M", "G", "T", "P", "E", "Z"]:
222 | if abs(num) < div:
223 | return f"{num:.{dec}f}{prefix}{unit}"
224 | num /= div
225 | return f"{num:.{dec}f}Y{unit}" # pragma: no cover
226 |
227 |
228 | def frmt_bytes(num: float, dec: int = 1) -> str: # pragma: no cover
229 | """Returns a formatted string of the number of bytes."""
230 | return frmt_num(num, dec, unit="iB", div=1024)
231 |
232 |
233 | def frmt_time(seconds: float, short: bool = False, width: int = 0): # pragma: no cover
234 | """Returns a formated string for a given time in seconds.
235 |
236 | Parameters
237 | ----------
238 | seconds : float
239 | Time value to format
240 | short : bool, optional
241 | Flag if short representation should be used.
242 | width : int, optional
243 | Optional minimum length of the returned string.
244 |
245 | Returns
246 | -------
247 | time_str: str
248 | """
249 | string = "00:00"
250 |
251 | # short time string
252 | if short:
253 | if seconds > 0:
254 | mins, secs = divmod(seconds, 60)
255 | if mins > 60:
256 | hours, mins = divmod(mins, 60)
257 | string = f"{hours:02.0f}:{mins:02.0f}h"
258 | else:
259 | string = f"{mins:02.0f}:{secs:02.0f}"
260 |
261 | # Full time strings
262 | else:
263 | if seconds < 1e-3:
264 | nanos = 1e6 * seconds
265 | string = f"{nanos:.0f}\u03BCs"
266 | elif seconds < 1:
267 | millis = 1000 * seconds
268 | string = f"{millis:.1f}ms"
269 | elif seconds < 60:
270 | string = f"{seconds:.1f}s"
271 | else:
272 | mins, seconds = divmod(seconds, 60)
273 | if mins < 60:
274 | string = f"{mins:.0f}:{seconds:04.1f}min"
275 | else:
276 | hours, mins = divmod(mins, 60)
277 | string = f"{hours:.0f}:{mins:02.0f}:{seconds:02.0f}h"
278 |
279 | if width > 0:
280 | string = f"{string:>{width}}"
281 | return string
282 |
--------------------------------------------------------------------------------
/lgtm.yml:
--------------------------------------------------------------------------------
1 | # LGTM configuration
2 |
3 | extraction:
4 | python: # Configure Python
5 | python_setup: # Configure the setup
6 | version: 3 # Specify Version 3
7 |
8 | path_classifiers:
9 | generated:
10 | - lattpy/_version.py # Set _version.py to a generated file
11 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # NOTE: you have to use single-quoted strings in TOML for regular expressions.
2 | # It's the equivalent of r-strings in Python. Multiline strings are treated as
3 | # verbose regular expressions by Black. Use [ ] to denote a significant space
4 | # character.
5 |
6 | # -- Build -----------------------------------------------------------------------------
7 |
8 | [build-system]
9 | requires = [
10 | "setuptools >= 60.0.0",
11 | "setuptools_scm[toml] >= 4",
12 | "setuptools_scm_git_archive",
13 | "wheel >= 0.37.0",
14 | ]
15 | build-backend = "setuptools.build_meta"
16 |
17 |
18 | [tool.setuptools_scm]
19 | write_to = "lattpy/_version.py"
20 | git_describe_command = "git describe --dirty --tags --long --match * --first-parent"
21 |
22 |
23 | # -- Black -----------------------------------------------------------------------------
24 |
25 | [tool.black]
26 |
27 | line-length = 88
28 | include = '\.pyi?$'
29 | exclude = "__main__.py|_version.py|renderer"
30 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | colorcet>=2.0.0
2 | hypothesis>=6.0.0
3 | matplotlib>=3.0.0
4 | numpy>=1.20.3
5 | pytest>=6.2.5
6 | scipy>=1.7.1
7 | setuptools-scm[toml]>=4
8 | setuptools>=60.0.0
9 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = lattpy
3 | description = Simple and efficient Python package for modeling d-dimensional Bravais lattices in solid state physics.
4 | long_description = file: README.md
5 | long_description_content_type = text/markdown
6 | url = https://github.com/dylanljones/lattpy
7 | author = Dylan Jones
8 | author_email = dylanljones94@gmail.com
9 | license = MIT
10 | license_files = LICENSE
11 | classifiers =
12 | Development Status :: 3 - Alpha
13 | Intended Audience :: Science/Research
14 | Topic :: Scientific/Engineering :: Physics
15 | Natural Language :: English
16 | License :: OSI Approved :: MIT License
17 | Programming Language :: Python :: 3
18 | Programming Language :: Python :: 3 :: Only
19 | Programming Language :: Python :: 3.7
20 | Programming Language :: Python :: 3.8
21 | Programming Language :: Python :: 3.9
22 | Programming Language :: Python :: 3.10
23 | Programming Language :: Python :: 3.11
24 | project_urls =
25 | Source = https://github.com/dylanljones/lattpy
26 | Documentation = https://lattpy.readthedocs.io/
27 | Tracker = https://github.com/dylanljones/lattpy/issues
28 |
29 | [options]
30 | packages = find:
31 | install_requires =
32 | setuptools>=60.0.0
33 | setuptools-scm[toml]>=4
34 | numpy>=1.20.3
35 | scipy>=1.7.1
36 | matplotlib>=3.0.0
37 | pytest>=6.2.5
38 | hypothesis>=6.0.0
39 | colorcet>=2.0.0
40 | python_requires = >=3.7
41 | include_package_data = True
42 | platforms = any
43 | zip_safe = False
44 |
45 | [options.extras_require]
46 | build =
47 | wheel>=0.37.0
48 | test =
49 | pytest-cov
50 |
51 | [aliases]
52 | test=pytest
53 |
54 |
55 | [build_sphinx]
56 | project = "LattPy"
57 | source-dir = ./docs/source
58 | build-dir = ./docs/build
59 |
60 |
61 | [pydocstyle]
62 | add-ignore = D105 # ignore undocumented dunder methods like ``__str__`
63 |
64 |
65 | [flake8]
66 | max-line-length = 88
67 | ignore = D203
68 | extend-ignore = E203
69 | per-file-ignores = __init__.py:F401
70 | exclude =
71 | .git,
72 | .idea,
73 | __pycache__,
74 | build,
75 | dist,
76 | lattpy/tests/*,
77 | docs/*,
78 | _version.py,
79 |
80 |
81 | [coverage:run]
82 | branch = False
83 | source = lattpy
84 |
85 | [coverage:report]
86 | exclude_lines =
87 | # Have to re-enable the standard pragma
88 | pragma: no cover
89 |
90 | # Don't complain about debug-only code and print statements:
91 | def __repr__
92 | def __str__
93 |
94 | # Don't complain about abstract methods
95 | @abstract
96 |
97 | # Ignore properties. These are usually simple getters
98 | @property
99 |
100 | # Don't complain if tests don't hit defensive assertion code:
101 | raise AssertionError
102 | raise NotImplementedError
103 |
104 | # Don't complain if non-runnable code isn't run:
105 | if 0:
106 | if __name__ == .__main__.:
107 |
108 | ignore_errors = True
109 |
110 | # Skip source files:
111 | omit =
112 | lattpy/tests/*
113 | lattpy/__init__.py
114 | lattpy/plotting.py
115 | lattpy/disptools.py
116 | lattpy/_version.py
117 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | import setuptools
4 |
5 |
6 | if __name__ == "__main__":
7 | # See `setup.cfg` and `pyproject.toml` for configuration.
8 | setuptools.setup()
9 |
--------------------------------------------------------------------------------