morpho.py 0.0.4__tar.gz → 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- morpho_py-0.1.0/PKG-INFO +24 -0
- morpho_py-0.1.0/morpho/__init__.py +28 -0
- morpho_py-0.1.0/morpho/brillouinzone.py +339 -0
- {morpho.py-0.0.4 → morpho_py-0.1.0}/morpho/geometry.py +54 -32
- {morpho.py-0.0.4 → morpho_py-0.1.0}/morpho/solver.py +80 -43
- {morpho.py-0.0.4 → morpho_py-0.1.0}/morpho/utils.py +22 -4
- morpho_py-0.1.0/pyproject.toml +64 -0
- morpho.py-0.0.4/PKG-INFO +0 -15
- morpho.py-0.0.4/morpho/__init__.py +0 -7
- morpho.py-0.0.4/morpho/brillouinzone.py +0 -253
- morpho.py-0.0.4/pyproject.toml +0 -26
- morpho.py-0.0.4/setup.py +0 -34
- {morpho.py-0.0.4 → morpho_py-0.1.0}/LICENSE +0 -0
- {morpho.py-0.0.4 → morpho_py-0.1.0}/morpho/exceptions.py +0 -0
morpho_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: morpho.py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PWE framework for photonic crystal analysis
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Tiago Vilela
|
|
8
|
+
Author-email: tiagovla@gmail.com
|
|
9
|
+
Requires-Python: >=3.9,<3.16
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
18
|
+
Provides-Extra: docs
|
|
19
|
+
Requires-Dist: matplotlib (>=3.4.3,<4.0.0)
|
|
20
|
+
Requires-Dist: mkdocs (>=1.6.1,<2) ; extra == "docs"
|
|
21
|
+
Requires-Dist: mkdocs-material (>=9.7.7) ; extra == "docs"
|
|
22
|
+
Requires-Dist: mkdocstrings[python] (>=0.24.0) ; extra == "docs"
|
|
23
|
+
Requires-Dist: numpy (>=1.20.1,<3.0.0)
|
|
24
|
+
Requires-Dist: scipy (>=1.6.2,<2.0.0)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
from morpho.brillouinzone import (
|
|
4
|
+
BrillouinZonePath,
|
|
5
|
+
BrillouinZonePath1D,
|
|
6
|
+
BrillouinZonePath2D,
|
|
7
|
+
BrillouinZonePath3D,
|
|
8
|
+
SymmetryPoint,
|
|
9
|
+
)
|
|
10
|
+
from morpho.geometry import Geometry, Geometry1D, Geometry2D, Geometry3D
|
|
11
|
+
from morpho.solver import Solver, Solver1D, Solver2D, Solver3D
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"__version__",
|
|
15
|
+
"BrillouinZonePath",
|
|
16
|
+
"BrillouinZonePath1D",
|
|
17
|
+
"BrillouinZonePath2D",
|
|
18
|
+
"BrillouinZonePath3D",
|
|
19
|
+
"SymmetryPoint",
|
|
20
|
+
"Geometry",
|
|
21
|
+
"Geometry1D",
|
|
22
|
+
"Geometry2D",
|
|
23
|
+
"Geometry3D",
|
|
24
|
+
"Solver",
|
|
25
|
+
"Solver1D",
|
|
26
|
+
"Solver2D",
|
|
27
|
+
"Solver3D",
|
|
28
|
+
]
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""This module implements classes related to the brillouinzone."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractproperty
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from numbers import Integral
|
|
6
|
+
from typing import List, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.linalg import norm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class BlochVectors:
|
|
14
|
+
"""Represents a data structure of Bloch wave vectors."""
|
|
15
|
+
|
|
16
|
+
values: np.ndarray
|
|
17
|
+
cumsum: np.ndarray
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SymmetryPoint:
|
|
21
|
+
"""Model of a symmetry point at the brillouinzone.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
point : Tuple[float, ...]
|
|
26
|
+
A point in the reciprocal domain.
|
|
27
|
+
name : str
|
|
28
|
+
A label to describe the point.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, point: Tuple[float, ...], name: str):
|
|
32
|
+
"""Initialize a SymmetryPoint."""
|
|
33
|
+
self.point = np.array(point)
|
|
34
|
+
self.name = name
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BrillouinZonePathBase(ABC):
|
|
38
|
+
"""BrillouinZoneBase class."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
path: List[SymmetryPoint],
|
|
43
|
+
n_points: int = 50,
|
|
44
|
+
strategy: str = "linear",
|
|
45
|
+
points_per_segment: Optional[int] = None,
|
|
46
|
+
):
|
|
47
|
+
self.path_points = path
|
|
48
|
+
self.n_points = n_points
|
|
49
|
+
self.strat = strategy
|
|
50
|
+
if points_per_segment is not None:
|
|
51
|
+
if (
|
|
52
|
+
not isinstance(points_per_segment, Integral)
|
|
53
|
+
or isinstance(points_per_segment, bool)
|
|
54
|
+
or points_per_segment < 1
|
|
55
|
+
):
|
|
56
|
+
raise ValueError("points_per_segment must be a positive integer.")
|
|
57
|
+
if len(path) < 2:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"At least two symmetry points are needed for segment sampling."
|
|
60
|
+
)
|
|
61
|
+
points_per_segment = int(points_per_segment)
|
|
62
|
+
self.points_per_segment = points_per_segment
|
|
63
|
+
self._path: np.ndarray
|
|
64
|
+
|
|
65
|
+
@abstractproperty
|
|
66
|
+
def betas(self) -> BlochVectors:
|
|
67
|
+
"""Return bloch wave vectors."""
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _interpolate_beta(beta_cs, path, n_points, points_per_segment=None):
|
|
71
|
+
"""Interpolate a path globally or by segment."""
|
|
72
|
+
if points_per_segment is not None:
|
|
73
|
+
beta_values = []
|
|
74
|
+
beta_distances = []
|
|
75
|
+
for index in range(path.shape[1] - 1):
|
|
76
|
+
fractions = np.linspace(0.0, 1.0, points_per_segment + 1)
|
|
77
|
+
start = path[:, index, None]
|
|
78
|
+
delta = (path[:, index + 1] - path[:, index])[:, None]
|
|
79
|
+
segment_values = start + delta * fractions[None, :]
|
|
80
|
+
|
|
81
|
+
start_distance = beta_cs[index]
|
|
82
|
+
segment_length = beta_cs[index + 1] - start_distance
|
|
83
|
+
segment_distances = start_distance + segment_length * fractions
|
|
84
|
+
|
|
85
|
+
if index:
|
|
86
|
+
segment_values = segment_values[:, 1:]
|
|
87
|
+
segment_distances = segment_distances[1:]
|
|
88
|
+
beta_values.append(segment_values)
|
|
89
|
+
beta_distances.append(segment_distances)
|
|
90
|
+
|
|
91
|
+
return BlochVectors(
|
|
92
|
+
np.concatenate(beta_values, axis=1),
|
|
93
|
+
np.concatenate(beta_distances),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
beta_csi = np.linspace(0, beta_cs[-1], n_points)
|
|
97
|
+
beta_vi = np.vstack(
|
|
98
|
+
[
|
|
99
|
+
np.interp(beta_csi, beta_cs, path[i, :]).flatten()
|
|
100
|
+
for i in range(path.shape[0])
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
return BlochVectors(beta_vi, beta_csi)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def point_locations(self):
|
|
107
|
+
"""Return cumulative sum of vectors' length."""
|
|
108
|
+
return np.concatenate(
|
|
109
|
+
(np.zeros(1), np.cumsum(norm(np.diff(self._path, axis=1), axis=0)))
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def point_names(self):
|
|
114
|
+
"""Return list of symmetry point names."""
|
|
115
|
+
return [p.name for p in self.path_points]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BrillouinZonePath1D(BrillouinZonePathBase):
|
|
119
|
+
"""BrillouinZonePath1D.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
a1 : Tuple[float]
|
|
124
|
+
Direct lattice vector a1.
|
|
125
|
+
path : List[SymmetryPoint]
|
|
126
|
+
List of symmetry points.
|
|
127
|
+
n_points : int
|
|
128
|
+
Total number of vectors sampled along the full path. Used when
|
|
129
|
+
``points_per_segment`` is not set.
|
|
130
|
+
strategy : str = 'linear'
|
|
131
|
+
Strategy to interpolate.
|
|
132
|
+
points_per_segment : int, optional
|
|
133
|
+
Number of intervals on each segment. Every symmetry point is sampled;
|
|
134
|
+
the total is one plus this value times the number of segments. This
|
|
135
|
+
takes precedence over ``n_points``.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
a1: Tuple[float],
|
|
141
|
+
path: List[SymmetryPoint],
|
|
142
|
+
n_points: int = 50,
|
|
143
|
+
strategy: str = "linear",
|
|
144
|
+
points_per_segment: Optional[int] = None,
|
|
145
|
+
):
|
|
146
|
+
"""Initialize a BrillouinZonePath."""
|
|
147
|
+
super().__init__(path, n_points, strategy, points_per_segment)
|
|
148
|
+
self.a1 = np.array(a1)
|
|
149
|
+
self.dim = 1
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def _path(self) -> np.ndarray:
|
|
153
|
+
"""Return the path matrix."""
|
|
154
|
+
path = np.stack([p.point for p in self.path_points], axis=1)
|
|
155
|
+
return path[0, :] * self.b1[:, None]
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def betas(self) -> BlochVectors:
|
|
159
|
+
"""Return beta vector values and cumsum."""
|
|
160
|
+
beta_cs = np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
161
|
+
beta_cs = np.pad(beta_cs, (1, 0), "constant")
|
|
162
|
+
if self.strat == "linear":
|
|
163
|
+
return self._interpolate_beta(
|
|
164
|
+
beta_cs, self._path, self.n_points, self.points_per_segment
|
|
165
|
+
)
|
|
166
|
+
raise NotImplementedError
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def b1(self) -> np.ndarray:
|
|
170
|
+
"""Return reciprocal lattice vector b1."""
|
|
171
|
+
return 2 * np.pi / self.a1
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class BrillouinZonePath2D(BrillouinZonePathBase):
|
|
175
|
+
"""BrillouinZonePath2D.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
a1 : Tuple[float, float]
|
|
180
|
+
Direct lattice vector a1.
|
|
181
|
+
a2 : Tuple[float, float]
|
|
182
|
+
Direct lattice vector a2.
|
|
183
|
+
path : List[SymmetryPoint]
|
|
184
|
+
List of symmetry points.
|
|
185
|
+
n_points : int
|
|
186
|
+
Total number of vectors sampled along the full path. Used when
|
|
187
|
+
``points_per_segment`` is not set.
|
|
188
|
+
strategy : str = 'linear'
|
|
189
|
+
Strategy to interpolate.
|
|
190
|
+
points_per_segment : int, optional
|
|
191
|
+
Number of intervals on each segment. Every symmetry point is sampled;
|
|
192
|
+
the total is one plus this value times the number of segments. This
|
|
193
|
+
takes precedence over ``n_points``.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
a1: Tuple[float, float],
|
|
199
|
+
a2: Tuple[float, float],
|
|
200
|
+
path: List[SymmetryPoint],
|
|
201
|
+
n_points: int = 50,
|
|
202
|
+
strategy: str = "linear",
|
|
203
|
+
points_per_segment: Optional[int] = None,
|
|
204
|
+
):
|
|
205
|
+
"""Initialize a BrillouinZonePath."""
|
|
206
|
+
super().__init__(path, n_points, strategy, points_per_segment)
|
|
207
|
+
self.a1 = np.array(a1)
|
|
208
|
+
self.a2 = np.array(a2)
|
|
209
|
+
self.dim = 2
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def _path(self) -> np.ndarray:
|
|
213
|
+
"""Return the path matrix."""
|
|
214
|
+
path = np.stack([p.point for p in self.path_points], axis=1)
|
|
215
|
+
return path[0, :] * self.b1[:, None] + path[1, :] * self.b2[:, None]
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def betas(self) -> BlochVectors:
|
|
219
|
+
"""Return beta vector values and cumsum."""
|
|
220
|
+
beta_cs = np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
221
|
+
beta_cs = np.pad(beta_cs, (1, 0), "constant")
|
|
222
|
+
if self.strat == "linear":
|
|
223
|
+
return self._interpolate_beta(
|
|
224
|
+
beta_cs, self._path, self.n_points, self.points_per_segment
|
|
225
|
+
)
|
|
226
|
+
raise NotImplementedError
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def b1(self) -> np.ndarray:
|
|
230
|
+
"""Return reciprocal lattice vector b1."""
|
|
231
|
+
Q = np.array([[0, -1], [1, 0]])
|
|
232
|
+
return 2 * np.pi * Q @ self.a2 / np.dot(self.a1, Q @ self.a2)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def b2(self) -> np.ndarray:
|
|
236
|
+
"""Return reciprocal lattice vector b2."""
|
|
237
|
+
Q = np.array([[0, -1], [1, 0]])
|
|
238
|
+
return 2 * np.pi * Q @ self.a1 / np.dot(self.a2, Q @ self.a1)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class BrillouinZonePath3D(BrillouinZonePathBase):
|
|
242
|
+
"""BrillouinZonePath3D.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
a1 : Tuple[float, float, float]
|
|
247
|
+
Direct lattice vector a1.
|
|
248
|
+
a2 : Tuple[float, float, float]
|
|
249
|
+
Direct lattice vector a2.
|
|
250
|
+
a3 : Tuple[float, float, float]
|
|
251
|
+
Direct lattice vector a3.
|
|
252
|
+
path : List[SymmetryPoint]
|
|
253
|
+
List of symmetry points.
|
|
254
|
+
n_points : int
|
|
255
|
+
Total number of vectors sampled along the full path. Used when
|
|
256
|
+
``points_per_segment`` is not set.
|
|
257
|
+
strategy : str = 'linear'
|
|
258
|
+
Strategy to interpolate.
|
|
259
|
+
points_per_segment : int, optional
|
|
260
|
+
Number of intervals on each segment. Every symmetry point is sampled;
|
|
261
|
+
the total is one plus this value times the number of segments. This
|
|
262
|
+
takes precedence over ``n_points``.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(
|
|
266
|
+
self,
|
|
267
|
+
a1: Tuple[float, float, float],
|
|
268
|
+
a2: Tuple[float, float, float],
|
|
269
|
+
a3: Tuple[float, float, float],
|
|
270
|
+
path: List[SymmetryPoint],
|
|
271
|
+
n_points: int = 50,
|
|
272
|
+
strategy: str = "interpolate",
|
|
273
|
+
points_per_segment: Optional[int] = None,
|
|
274
|
+
):
|
|
275
|
+
"""Initialize a BrillouinZonePath."""
|
|
276
|
+
super().__init__(path, n_points, strategy, points_per_segment)
|
|
277
|
+
self.a1 = np.array(a1)
|
|
278
|
+
self.a2 = np.array(a2)
|
|
279
|
+
self.a3 = np.array(a3)
|
|
280
|
+
self.dim = 3
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def _path(self) -> np.ndarray:
|
|
284
|
+
"""Return the path matrix."""
|
|
285
|
+
path = np.stack([p.point for p in self.path_points], axis=1)
|
|
286
|
+
return (
|
|
287
|
+
path[0, :] * self.b1[:, None]
|
|
288
|
+
+ path[1, :] * self.b2[:, None]
|
|
289
|
+
+ path[2, :] * self.b3[:, None]
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def betas(self) -> BlochVectors:
|
|
294
|
+
"""Return beta vector values and cumsum."""
|
|
295
|
+
beta_cs = np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
296
|
+
beta_cs = np.pad(beta_cs, (1, 0), "constant")
|
|
297
|
+
if self.strat == "interpolate":
|
|
298
|
+
return self._interpolate_beta(
|
|
299
|
+
beta_cs, self._path, self.n_points, self.points_per_segment
|
|
300
|
+
)
|
|
301
|
+
raise NotImplementedError
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def b1(self) -> np.ndarray:
|
|
305
|
+
"""Return reciprocal lattice vector b1."""
|
|
306
|
+
return (
|
|
307
|
+
2
|
|
308
|
+
* np.pi
|
|
309
|
+
* np.cross(self.a2, self.a3)
|
|
310
|
+
/ np.dot(self.a1, np.cross(self.a2, self.a3))
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def b2(self) -> np.ndarray:
|
|
315
|
+
"""Return reciprocal lattice vector b2."""
|
|
316
|
+
return (
|
|
317
|
+
2
|
|
318
|
+
* np.pi
|
|
319
|
+
* np.cross(self.a3, self.a1)
|
|
320
|
+
/ np.dot(self.a1, np.cross(self.a2, self.a3))
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def b3(self) -> np.ndarray:
|
|
325
|
+
"""Return reciprocal lattice vector b3."""
|
|
326
|
+
return (
|
|
327
|
+
2
|
|
328
|
+
* np.pi
|
|
329
|
+
* np.cross(self.a1, self.a2)
|
|
330
|
+
/ np.dot(self.a1, np.cross(self.a2, self.a3))
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def BrillouinZonePath(
|
|
335
|
+
a1, *args, **kwargs
|
|
336
|
+
) -> Union[BrillouinZonePath1D, BrillouinZonePath2D, BrillouinZonePath3D]:
|
|
337
|
+
"""BrillouinZonePath factory."""
|
|
338
|
+
dim_obj = {1: BrillouinZonePath1D, 2: BrillouinZonePath2D, 3: BrillouinZonePath3D}
|
|
339
|
+
return dim_obj[len(a1)](a1, *args, **kwargs)
|
|
@@ -26,7 +26,7 @@ class GeometryBase(ABC):
|
|
|
26
26
|
func : Callable
|
|
27
27
|
func
|
|
28
28
|
"""
|
|
29
|
-
if func.__name__ in [
|
|
29
|
+
if func.__name__ in ["eps_rf", "mu_rf"]:
|
|
30
30
|
setattr(self, func.__name__, func)
|
|
31
31
|
else:
|
|
32
32
|
raise Exception("Can only overwrite eps_rf or mu_rf.")
|
|
@@ -47,24 +47,40 @@ class Geometry1D(GeometryBase):
|
|
|
47
47
|
Permeabillity matrix mu_r if directly supplied.
|
|
48
48
|
"""
|
|
49
49
|
|
|
50
|
-
def __init__(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
a1: Tuple[float],
|
|
53
|
+
n1: int = 64,
|
|
54
|
+
eps_rf: Optional[Callable] = None,
|
|
55
|
+
mu_rf: Optional[Callable] = None,
|
|
56
|
+
):
|
|
55
57
|
super().__init__(eps_rf, mu_rf)
|
|
56
58
|
self.a1: np.ndarray = np.array(a1)
|
|
57
59
|
self.n1: int = n1
|
|
58
|
-
self._eps_r: np.ndarray = np.ones((n1,
|
|
59
|
-
self._mu_r: np.ndarray = np.ones((n1,
|
|
60
|
+
self._eps_r: np.ndarray = np.ones((n1,), dtype=complex)
|
|
61
|
+
self._mu_r: np.ndarray = np.ones((n1,), dtype=complex)
|
|
60
62
|
|
|
61
63
|
@property
|
|
62
64
|
def X(self) -> CartesianVectors1D:
|
|
63
65
|
"""Return cartesian positions."""
|
|
64
|
-
P0 = np.
|
|
66
|
+
P0 = np.arange(self.n1) / self.n1 - 0.5
|
|
65
67
|
x = P0 * self.a1[0]
|
|
66
68
|
return CartesianVectors1D(x)
|
|
67
69
|
|
|
70
|
+
@property
|
|
71
|
+
def eps_r(self) -> np.ndarray:
|
|
72
|
+
"""Return the relative permittivity profile."""
|
|
73
|
+
if self.eps_rf:
|
|
74
|
+
self.eps_rf(eps_r=self._eps_r, x=self.X.x)
|
|
75
|
+
return self._eps_r
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def mu_r(self) -> np.ndarray:
|
|
79
|
+
"""Return the relative permeability profile."""
|
|
80
|
+
if self.mu_rf:
|
|
81
|
+
self.mu_rf(self._mu_r, x=self.X.x)
|
|
82
|
+
return self._mu_r
|
|
83
|
+
|
|
68
84
|
|
|
69
85
|
class Geometry2D(GeometryBase):
|
|
70
86
|
"""Geometry class.
|
|
@@ -85,13 +101,15 @@ class Geometry2D(GeometryBase):
|
|
|
85
101
|
Permeabillity matrix mu_r if directly supplied.
|
|
86
102
|
"""
|
|
87
103
|
|
|
88
|
-
def __init__(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
a1: Tuple[float, float],
|
|
107
|
+
a2: Tuple[float, float],
|
|
108
|
+
n1: int = 64,
|
|
109
|
+
n2: int = 64,
|
|
110
|
+
eps_rf: Optional[Callable] = None,
|
|
111
|
+
mu_rf: Optional[Callable] = None,
|
|
112
|
+
):
|
|
95
113
|
super().__init__(eps_rf, mu_rf)
|
|
96
114
|
self.a1: np.ndarray = np.array(a1)
|
|
97
115
|
self.a2: np.ndarray = np.array(a2)
|
|
@@ -104,8 +122,9 @@ class Geometry2D(GeometryBase):
|
|
|
104
122
|
def X(self) -> CartesianVectors2D:
|
|
105
123
|
"""Return cartesian positions."""
|
|
106
124
|
P0, Q0 = np.meshgrid(
|
|
107
|
-
np.
|
|
108
|
-
np.
|
|
125
|
+
np.arange(self.n1) / self.n1 - 0.5,
|
|
126
|
+
np.arange(self.n2) / self.n2 - 0.5,
|
|
127
|
+
indexing="ij",
|
|
109
128
|
)
|
|
110
129
|
|
|
111
130
|
x = P0 * self.a1[0] + Q0 * self.a2[0]
|
|
@@ -124,7 +143,7 @@ class Geometry2D(GeometryBase):
|
|
|
124
143
|
def mu_r(self) -> np.ndarray:
|
|
125
144
|
"""Return the relative permeabillity matrice."""
|
|
126
145
|
if self.mu_rf:
|
|
127
|
-
self.mu_rf(
|
|
146
|
+
self.mu_rf(self._mu_r, x=self.X.x, y=self.X.y)
|
|
128
147
|
return self._mu_r
|
|
129
148
|
|
|
130
149
|
|
|
@@ -151,15 +170,17 @@ class Geometry3D(GeometryBase):
|
|
|
151
170
|
Permeabillity matrix function mu_rf if directly supplied.
|
|
152
171
|
"""
|
|
153
172
|
|
|
154
|
-
def __init__(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
a1: Tuple[float, float, float],
|
|
176
|
+
a2: Tuple[float, float, float],
|
|
177
|
+
a3: Tuple[float, float, float],
|
|
178
|
+
n1: int = 64,
|
|
179
|
+
n2: int = 64,
|
|
180
|
+
n3: int = 64,
|
|
181
|
+
eps_rf: Optional[Callable] = None,
|
|
182
|
+
mu_rf: Optional[Callable] = None,
|
|
183
|
+
):
|
|
163
184
|
super().__init__(eps_rf, mu_rf)
|
|
164
185
|
self.a1: np.ndarray = np.array(a1)
|
|
165
186
|
self.a2: np.ndarray = np.array(a2)
|
|
@@ -174,9 +195,10 @@ class Geometry3D(GeometryBase):
|
|
|
174
195
|
def X(self) -> CartesianVectors3D:
|
|
175
196
|
"""Return cartesian positions."""
|
|
176
197
|
P0, Q0, R0 = np.meshgrid(
|
|
177
|
-
np.
|
|
178
|
-
np.
|
|
179
|
-
np.
|
|
198
|
+
np.arange(self.n1) / self.n1 - 0.5,
|
|
199
|
+
np.arange(self.n2) / self.n2 - 0.5,
|
|
200
|
+
np.arange(self.n3) / self.n3 - 0.5,
|
|
201
|
+
indexing="ij",
|
|
180
202
|
)
|
|
181
203
|
|
|
182
204
|
x = P0 * self.a1[0] + Q0 * self.a2[0] + R0 * self.a3[0]
|
|
@@ -196,7 +218,7 @@ class Geometry3D(GeometryBase):
|
|
|
196
218
|
def mu_r(self) -> np.ndarray:
|
|
197
219
|
"""Return the relative permeabillity matrice."""
|
|
198
220
|
if self.mu_rf:
|
|
199
|
-
self.mu_rf(
|
|
221
|
+
self.mu_rf(self._mu_r, x=self.X.x, y=self.X.y, z=self.X.z)
|
|
200
222
|
return self._mu_r
|
|
201
223
|
|
|
202
224
|
|
|
@@ -9,7 +9,7 @@ from .brillouinzone import BrillouinZonePath1D as BZPath1D
|
|
|
9
9
|
from .brillouinzone import BrillouinZonePath2D as BZPath2D
|
|
10
10
|
from .brillouinzone import BrillouinZonePath3D as BZPath3D
|
|
11
11
|
from .geometry import Geometry1D, Geometry2D, Geometry3D
|
|
12
|
-
from .utils import convmat
|
|
12
|
+
from .utils import convmat, harmonic_indices
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Solver1D:
|
|
@@ -25,11 +25,9 @@ class Solver1D:
|
|
|
25
25
|
Number of terms in the direction of the reciprocal vector b1.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
def __init__(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
P: int = 1,
|
|
32
|
-
pol: str = "TM"):
|
|
28
|
+
def __init__(
|
|
29
|
+
self, geometry: Geometry1D, path: BZPath1D, P: int = 1, pol: str = "TM"
|
|
30
|
+
):
|
|
33
31
|
"""Initialize the PWE Solver."""
|
|
34
32
|
self.geo = geometry
|
|
35
33
|
self.path = path
|
|
@@ -43,7 +41,29 @@ class Solver1D:
|
|
|
43
41
|
|
|
44
42
|
def run(self):
|
|
45
43
|
"""Calculate the eigen-wavenumbers and modes."""
|
|
46
|
-
|
|
44
|
+
self.eps_rc = convmat(self.geo.eps_r, self.P)
|
|
45
|
+
self.mu_rc = convmat(self.geo.mu_r, self.P)
|
|
46
|
+
mu_rci = convmat(1 / self.geo.mu_r, self.P)
|
|
47
|
+
eps_rci = convmat(1 / self.geo.eps_r, self.P)
|
|
48
|
+
|
|
49
|
+
p = harmonic_indices(self.P)
|
|
50
|
+
G_k = p * self.path.b1[0]
|
|
51
|
+
beta = self.path.betas.values
|
|
52
|
+
|
|
53
|
+
for col in range(beta.shape[1]):
|
|
54
|
+
k = beta[0, col] - G_k
|
|
55
|
+
KX = np.diag(k)
|
|
56
|
+
|
|
57
|
+
if self.pol == "TM":
|
|
58
|
+
A = KX @ mu_rci @ KX
|
|
59
|
+
B = self.eps_rc
|
|
60
|
+
else:
|
|
61
|
+
A = KX @ eps_rci @ KX
|
|
62
|
+
B = self.mu_rc
|
|
63
|
+
|
|
64
|
+
D, V = eigh(A, B)
|
|
65
|
+
self.wn.append(np.sqrt(np.maximum(D, 0)))
|
|
66
|
+
self.modes.append(V)
|
|
47
67
|
|
|
48
68
|
|
|
49
69
|
class Solver2D:
|
|
@@ -61,12 +81,14 @@ class Solver2D:
|
|
|
61
81
|
Number of terms in the direction of the reciprocal vector b2.
|
|
62
82
|
"""
|
|
63
83
|
|
|
64
|
-
def __init__(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
geometry: Geometry2D,
|
|
87
|
+
path: BZPath2D,
|
|
88
|
+
P: int = 1,
|
|
89
|
+
Q: int = 1,
|
|
90
|
+
pol: str = "TM",
|
|
91
|
+
):
|
|
70
92
|
"""Initialize the PWE Solver."""
|
|
71
93
|
self.geo = geometry
|
|
72
94
|
self.path = path
|
|
@@ -83,16 +105,20 @@ class Solver2D:
|
|
|
83
105
|
self.eps_rc = convmat(self.geo.eps_r, self.P, self.Q)
|
|
84
106
|
self.mu_rc = convmat(self.geo.mu_r, self.P, self.Q)
|
|
85
107
|
|
|
86
|
-
mu_rci =
|
|
87
|
-
|
|
108
|
+
mu_rci = convmat(1 / self.geo.mu_r, self.P, self.Q)
|
|
109
|
+
if self.pol != "TM":
|
|
110
|
+
# Factorize epsilon before truncating its reciprocal. This
|
|
111
|
+
# inverse rule converges much faster for TE modes at sharp
|
|
112
|
+
# dielectric interfaces than Fourier coefficients of 1/eps.
|
|
113
|
+
eps_rci = np.linalg.inv(self.eps_rc)
|
|
88
114
|
|
|
89
|
-
p =
|
|
90
|
-
q =
|
|
115
|
+
p = harmonic_indices(self.P)
|
|
116
|
+
q = harmonic_indices(self.Q)
|
|
91
117
|
|
|
92
|
-
P0, Q0 = np.meshgrid(p, q)
|
|
118
|
+
P0, Q0 = np.meshgrid(p, q, indexing="ij")
|
|
93
119
|
b1, b2 = self.path.b1, self.path.b2
|
|
94
120
|
|
|
95
|
-
G_k = P0.
|
|
121
|
+
G_k = P0.ravel(order="F") * b1[:, None] + Q0.ravel(order="F") * b2[:, None]
|
|
96
122
|
beta = self.path.betas.values
|
|
97
123
|
|
|
98
124
|
for col in range(beta.shape[1]):
|
|
@@ -102,10 +128,10 @@ class Solver2D:
|
|
|
102
128
|
KY = np.diag(k[1, :])
|
|
103
129
|
|
|
104
130
|
if self.pol == "TM":
|
|
105
|
-
A = KX@mu_rci@KX + KY@mu_rci@KY
|
|
131
|
+
A = KX @ mu_rci @ KX + KY @ mu_rci @ KY
|
|
106
132
|
B = self.eps_rc
|
|
107
133
|
else:
|
|
108
|
-
A = KX@eps_rci@KX + KY@eps_rci@KY
|
|
134
|
+
A = KX @ eps_rci @ KX + KY @ eps_rci @ KY
|
|
109
135
|
B = self.mu_rc
|
|
110
136
|
|
|
111
137
|
D, V = eigh(A, B)
|
|
@@ -131,12 +157,9 @@ class Solver3D:
|
|
|
131
157
|
Number of terms in the direction of the reciprocal vector b3.
|
|
132
158
|
"""
|
|
133
159
|
|
|
134
|
-
def __init__(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
P: int = 1,
|
|
138
|
-
Q: int = 1,
|
|
139
|
-
R: int = 1):
|
|
160
|
+
def __init__(
|
|
161
|
+
self, geometry: Geometry3D, path: BZPath3D, P: int = 1, Q: int = 1, R: int = 1
|
|
162
|
+
):
|
|
140
163
|
"""Initialize the PWE Solver."""
|
|
141
164
|
self.geo = geometry
|
|
142
165
|
self.path = path
|
|
@@ -153,17 +176,21 @@ class Solver3D:
|
|
|
153
176
|
self.mu_rc = convmat(self.geo.mu_r, self.P, self.Q, self.R)
|
|
154
177
|
|
|
155
178
|
eps_rk = np.kron(np.eye(3), self.eps_rc)
|
|
156
|
-
|
|
179
|
+
mu_ri = convmat(1 / self.geo.mu_r, self.P, self.Q, self.R)
|
|
180
|
+
mu_rki = np.kron(np.eye(3), mu_ri)
|
|
157
181
|
|
|
158
|
-
p =
|
|
159
|
-
q =
|
|
160
|
-
r =
|
|
182
|
+
p = harmonic_indices(self.P)
|
|
183
|
+
q = harmonic_indices(self.Q)
|
|
184
|
+
r = harmonic_indices(self.R)
|
|
161
185
|
|
|
162
|
-
P0, Q0, R0 = np.meshgrid(p, q, r)
|
|
186
|
+
P0, Q0, R0 = np.meshgrid(p, q, r, indexing="ij")
|
|
163
187
|
b1, b2, b3 = self.path.b1, self.path.b2, self.path.b3
|
|
164
188
|
|
|
165
|
-
G_k = (
|
|
166
|
-
|
|
189
|
+
G_k = (
|
|
190
|
+
b1[:, None] * P0.ravel(order="F")
|
|
191
|
+
+ b2[:, None] * Q0.ravel(order="F")
|
|
192
|
+
+ b3[:, None] * R0.ravel(order="F")
|
|
193
|
+
)
|
|
167
194
|
|
|
168
195
|
beta = self.path.betas.values
|
|
169
196
|
for col in range(beta.shape[1]):
|
|
@@ -172,22 +199,32 @@ class Solver3D:
|
|
|
172
199
|
KX = np.diag(k[0, :])
|
|
173
200
|
KY = np.diag(k[1, :])
|
|
174
201
|
KZ = np.diag(k[2, :])
|
|
175
|
-
K_V = np.vstack(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
K_V = np.vstack(
|
|
203
|
+
(
|
|
204
|
+
np.hstack((0 * KX, -KZ, KY)),
|
|
205
|
+
np.hstack((KZ, 0 * KY, -KX)),
|
|
206
|
+
np.hstack((-KY, KX, 0 * KZ)),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
180
209
|
A = K_V @ mu_rki @ K_V
|
|
181
210
|
B = eps_rk
|
|
182
211
|
|
|
183
212
|
D, V = eigh(A, B)
|
|
184
213
|
|
|
185
|
-
|
|
186
|
-
|
|
214
|
+
# The curl-curl operator has one longitudinal null mode per
|
|
215
|
+
# retained reciprocal vector. Drop those N zero eigenvalues;
|
|
216
|
+
# at Gamma the two physical zero-frequency modes remain.
|
|
217
|
+
n_harmonics = KX.shape[0]
|
|
218
|
+
D = D[:-n_harmonics]
|
|
219
|
+
V = V[:, :-n_harmonics]
|
|
220
|
+
|
|
221
|
+
# eigh returns the negative eigenvalues from most negative to
|
|
222
|
+
# least negative, so reverse to report frequencies low to high.
|
|
223
|
+
self.wn.append(np.sqrt(np.maximum(-D, 0))[::-1])
|
|
224
|
+
self.modes.append(V[:, ::-1])
|
|
187
225
|
|
|
188
226
|
|
|
189
|
-
def Solver(geometry, path, *args,
|
|
190
|
-
**kwargs) -> Union[Solver1D, Solver2D, Solver3D]:
|
|
227
|
+
def Solver(geometry, path, *args, **kwargs) -> Union[Solver1D, Solver2D, Solver3D]:
|
|
191
228
|
"""Solver factory."""
|
|
192
229
|
dim_obj = {1: Solver1D, 2: Solver2D, 3: Solver3D}
|
|
193
230
|
return dim_obj[path.dim](geometry, path, *args, **kwargs)
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"""This module implements utilities."""
|
|
2
|
+
|
|
2
3
|
import numpy as np
|
|
3
4
|
|
|
4
5
|
|
|
6
|
+
def harmonic_indices(count: int) -> np.ndarray:
|
|
7
|
+
"""Return ``count`` integer harmonics centered as closely as possible at 0.
|
|
8
|
+
|
|
9
|
+
For an even count, the integer range is necessarily asymmetric; this
|
|
10
|
+
convention keeps zero and the negative Nyquist-side harmonic.
|
|
11
|
+
"""
|
|
12
|
+
if count < 1:
|
|
13
|
+
raise ValueError("The number of harmonics must be positive.")
|
|
14
|
+
return np.arange(-(count // 2), (count + 1) // 2)
|
|
15
|
+
|
|
16
|
+
|
|
5
17
|
def convmat(A: np.ndarray, P: int = 1, Q: int = 1, R: int = 1):
|
|
6
18
|
"""Return a permittivity/permeability convolution matrix.
|
|
7
19
|
|
|
@@ -17,12 +29,18 @@ def convmat(A: np.ndarray, P: int = 1, Q: int = 1, R: int = 1):
|
|
|
17
29
|
Number of terms in the direction of the reciprocal vector T3.
|
|
18
30
|
"""
|
|
19
31
|
NH = P * Q * R
|
|
20
|
-
A = np.
|
|
32
|
+
A = np.asarray(A)
|
|
33
|
+
if A.ndim == 1:
|
|
34
|
+
A = A[:, None, None]
|
|
35
|
+
elif A.ndim == 2:
|
|
36
|
+
A = A[:, :, None]
|
|
37
|
+
elif A.ndim != 3:
|
|
38
|
+
raise ValueError("A must have one, two, or three dimensions.")
|
|
21
39
|
N1, N2, N3 = A.shape
|
|
22
40
|
|
|
23
|
-
p =
|
|
24
|
-
q =
|
|
25
|
-
r =
|
|
41
|
+
p = harmonic_indices(P)
|
|
42
|
+
q = harmonic_indices(Q)
|
|
43
|
+
r = harmonic_indices(R)
|
|
26
44
|
|
|
27
45
|
A = np.fft.fftshift(np.fft.fftn(A) / A.size)
|
|
28
46
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "morpho.py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "PWE framework for photonic crystal analysis"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
authors = [{name = "Tiago Vilela", email = "tiagovla@gmail.com"}]
|
|
7
|
+
requires-python = ">=3.9,<3.16"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Programming Language :: Python :: 3",
|
|
10
|
+
"Programming Language :: Python :: 3.9",
|
|
11
|
+
"Programming Language :: Python :: 3.10",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Programming Language :: Python :: 3.15",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"numpy>=1.20.1,<3.0.0",
|
|
20
|
+
"scipy>=1.6.2,<2.0.0",
|
|
21
|
+
"matplotlib>=3.4.3,<4.0.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
docs = [
|
|
26
|
+
"mkdocs>=1.6.1,<2",
|
|
27
|
+
"mkdocs-material>=9.7.7",
|
|
28
|
+
"mkdocstrings[python]>=0.24.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.poetry]
|
|
32
|
+
packages = [
|
|
33
|
+
{include = "morpho", from = "."}
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"black==24.8.0",
|
|
39
|
+
"pre-commit>=3.8.0,<4.0.0",
|
|
40
|
+
"pytest>=5.2",
|
|
41
|
+
"ruff==0.16.10",
|
|
42
|
+
"mkdocs>=1.6.1,<2",
|
|
43
|
+
"mkdocs-material>=9.7.7",
|
|
44
|
+
"mkdocstrings[python]>=0.24.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.uv]
|
|
48
|
+
package = true
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["poetry-core>=2.0.0"]
|
|
52
|
+
build-backend = "poetry.core.masonry.api"
|
|
53
|
+
|
|
54
|
+
[tool.black]
|
|
55
|
+
line-length = 88
|
|
56
|
+
target-version = ["py39"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 88
|
|
60
|
+
target-version = "py39"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E4", "E7", "E9", "F"]
|
|
64
|
+
|
morpho.py-0.0.4/PKG-INFO
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: morpho.py
|
|
3
|
-
Version: 0.0.4
|
|
4
|
-
Summary:
|
|
5
|
-
License: MIT
|
|
6
|
-
Author: Tiago Vilela
|
|
7
|
-
Author-email: tiagovla@gmail.com
|
|
8
|
-
Requires-Python: >=3.8,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
-
Requires-Dist: matplotlib (>=3.3.4,<4.0.0)
|
|
14
|
-
Requires-Dist: numpy (>=1.20.1,<2.0.0)
|
|
15
|
-
Requires-Dist: scipy (>=1.6.1,<2.0.0)
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.4"
|
|
2
|
-
|
|
3
|
-
from morpho.brillouinzone import (BrillouinZonePath, BrillouinZonePath1D,
|
|
4
|
-
BrillouinZonePath2D, BrillouinZonePath3D,
|
|
5
|
-
SymmetryPoint)
|
|
6
|
-
from morpho.geometry import Geometry, Geometry1D, Geometry2D, Geometry3D
|
|
7
|
-
from morpho.solver import Solver, Solver1D, Solver2D, Solver3D
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
"""This module implements classes related to the brillouinzone."""
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractproperty
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from typing import List, Tuple, Union
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
|
-
from numpy.linalg import norm
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class BlochVectors:
|
|
13
|
-
"""Represents a data structure of Bloch wave vectors."""
|
|
14
|
-
|
|
15
|
-
values: np.ndarray
|
|
16
|
-
cumsum: np.ndarray
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SymmetryPoint:
|
|
20
|
-
"""Model of a symmetry point at the brillouinzone.
|
|
21
|
-
|
|
22
|
-
Parameters
|
|
23
|
-
----------
|
|
24
|
-
point : Tuple[float, ...]
|
|
25
|
-
A point in the reciprocal domain.
|
|
26
|
-
name : str
|
|
27
|
-
A label to describe the point.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, point: Tuple[float, ...], name: str):
|
|
31
|
-
"""Initialize a SymmetryPoint."""
|
|
32
|
-
self.point = np.array(point)
|
|
33
|
-
self.name = name
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class BrillouinZonePathBase(ABC):
|
|
37
|
-
"""BrillouinZoneBase class."""
|
|
38
|
-
|
|
39
|
-
def __init__(self,
|
|
40
|
-
path: List[SymmetryPoint],
|
|
41
|
-
n_points: int = 50,
|
|
42
|
-
strategy: str = 'linear'):
|
|
43
|
-
self.path_points = path
|
|
44
|
-
self.n_points = n_points
|
|
45
|
-
self.strat = strategy
|
|
46
|
-
self._path: np.ndarray
|
|
47
|
-
|
|
48
|
-
@abstractproperty
|
|
49
|
-
def betas(self) -> BlochVectors:
|
|
50
|
-
"""Return bloch wave vectors."""
|
|
51
|
-
|
|
52
|
-
@staticmethod
|
|
53
|
-
def _interpolate_beta(beta_cs, path, n_points):
|
|
54
|
-
"""Perform linear interpolation."""
|
|
55
|
-
beta_csi = np.linspace(0, beta_cs[-1], n_points)
|
|
56
|
-
beta_vi = np.vstack([
|
|
57
|
-
np.interp(beta_csi, beta_cs, path[i, :]).flatten()
|
|
58
|
-
for i in range(path.shape[0])
|
|
59
|
-
])
|
|
60
|
-
return BlochVectors(beta_vi, beta_csi)
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def point_locations(self):
|
|
64
|
-
"""Return cumulative sum of vectors' length."""
|
|
65
|
-
return np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def point_names(self):
|
|
69
|
-
"""Return list of symmetry point names."""
|
|
70
|
-
return [p.name for p in self.path_points]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class BrillouinZonePath1D(BrillouinZonePathBase):
|
|
74
|
-
"""BrillouinZonePath1D.
|
|
75
|
-
|
|
76
|
-
Parameters
|
|
77
|
-
----------
|
|
78
|
-
a1 : Tuple[float]
|
|
79
|
-
Direct lattice vector a1.
|
|
80
|
-
path : List[SymmetryPoint]
|
|
81
|
-
List of symmetry points.
|
|
82
|
-
n_points : int
|
|
83
|
-
Number of vectors.
|
|
84
|
-
strategy : str = 'linear'
|
|
85
|
-
Strategy to interpolate.
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
def __init__(self,
|
|
89
|
-
a1: Tuple[float],
|
|
90
|
-
path: List[SymmetryPoint],
|
|
91
|
-
n_points: int = 50,
|
|
92
|
-
strategy: str = 'linear'):
|
|
93
|
-
"""Initialize a BrillouinZonePath."""
|
|
94
|
-
super().__init__(path, n_points, strategy)
|
|
95
|
-
self.a1 = np.array(a1)
|
|
96
|
-
self.dim = 1
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def _path(self) -> np.ndarray:
|
|
100
|
-
"""Return the path matrix."""
|
|
101
|
-
path = np.stack([p.point for p in self.path_points], axis=1)
|
|
102
|
-
return path[0, :] * self.b1[:, None]
|
|
103
|
-
|
|
104
|
-
@property
|
|
105
|
-
def betas(self) -> BlochVectors:
|
|
106
|
-
"""Return beta vector values and cumsum."""
|
|
107
|
-
beta_cs = np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
108
|
-
beta_cs = np.pad(beta_cs, (1, 0), "constant")
|
|
109
|
-
if self.strat == 'linear':
|
|
110
|
-
return self._interpolate_beta(beta_cs, self._path, self.n_points)
|
|
111
|
-
raise NotImplementedError
|
|
112
|
-
|
|
113
|
-
@property
|
|
114
|
-
def b1(self) -> np.ndarray:
|
|
115
|
-
"""Return reciprocal lattice vector b1."""
|
|
116
|
-
return 2 * np.pi / self.a1
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class BrillouinZonePath2D(BrillouinZonePathBase):
|
|
120
|
-
"""BrillouinZonePath2D.
|
|
121
|
-
|
|
122
|
-
Parameters
|
|
123
|
-
----------
|
|
124
|
-
a1 : Tuple[float, float]
|
|
125
|
-
Direct lattice vector a1.
|
|
126
|
-
a2 : Tuple[float, float]
|
|
127
|
-
Direct lattice vector a2.
|
|
128
|
-
path : List[SymmetryPoint]
|
|
129
|
-
List of symmetry points.
|
|
130
|
-
n_points : int
|
|
131
|
-
Number of vectors.
|
|
132
|
-
strategy : str = 'linear'
|
|
133
|
-
Strategy to interpolate.
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
def __init__(self,
|
|
137
|
-
a1: Tuple[float, float],
|
|
138
|
-
a2: Tuple[float, float],
|
|
139
|
-
path: List[SymmetryPoint],
|
|
140
|
-
n_points: int = 50,
|
|
141
|
-
strategy: str = 'linear'):
|
|
142
|
-
"""Initialize a BrillouinZonePath."""
|
|
143
|
-
super().__init__(path, n_points, strategy)
|
|
144
|
-
self.a1 = np.array(a1)
|
|
145
|
-
self.a2 = np.array(a2)
|
|
146
|
-
self.dim = 2
|
|
147
|
-
|
|
148
|
-
@property
|
|
149
|
-
def _path(self) -> np.ndarray:
|
|
150
|
-
"""Return the path matrix."""
|
|
151
|
-
path = np.stack([p.point for p in self.path_points], axis=1)
|
|
152
|
-
return (path[0, :] * self.b1[:, None] + path[1, :] * self.b2[:, None])
|
|
153
|
-
|
|
154
|
-
@property
|
|
155
|
-
def betas(self) -> BlochVectors:
|
|
156
|
-
"""Return beta vector values and cumsum."""
|
|
157
|
-
beta_cs = np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
158
|
-
beta_cs = np.pad(beta_cs, (1, 0), "constant")
|
|
159
|
-
if self.strat == 'linear':
|
|
160
|
-
return self._interpolate_beta(beta_cs, self._path, self.n_points)
|
|
161
|
-
raise NotImplementedError
|
|
162
|
-
|
|
163
|
-
@property
|
|
164
|
-
def b1(self) -> np.ndarray:
|
|
165
|
-
"""Return reciprocal lattice vector b1."""
|
|
166
|
-
Q = np.array([[0, -1], [1, 0]])
|
|
167
|
-
return 2 * np.pi * Q @ self.a2 / np.dot(self.a1, Q @ self.a2)
|
|
168
|
-
|
|
169
|
-
@property
|
|
170
|
-
def b2(self) -> np.ndarray:
|
|
171
|
-
"""Return reciprocal lattice vector b2."""
|
|
172
|
-
Q = np.array([[0, -1], [1, 0]])
|
|
173
|
-
return 2 * np.pi * Q @ self.a1 / np.dot(self.a2, Q @ self.a1)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
class BrillouinZonePath3D(BrillouinZonePathBase):
|
|
177
|
-
"""BrillouinZonePath3D.
|
|
178
|
-
|
|
179
|
-
Parameters
|
|
180
|
-
----------
|
|
181
|
-
a1 : Tuple[float, float, float]
|
|
182
|
-
Direct lattice vector a1.
|
|
183
|
-
a2 : Tuple[float, float, float]
|
|
184
|
-
Direct lattice vector a2.
|
|
185
|
-
a3 : Tuple[float, float, float]
|
|
186
|
-
Direct lattice vector a3.
|
|
187
|
-
path : List[SymmetryPoint]
|
|
188
|
-
List of symmetry points.
|
|
189
|
-
n_points : int
|
|
190
|
-
Number of vectors.
|
|
191
|
-
strategy : str = 'linear'
|
|
192
|
-
Strategy to interpolate.
|
|
193
|
-
"""
|
|
194
|
-
|
|
195
|
-
def __init__(self,
|
|
196
|
-
a1: Tuple[float, float, float],
|
|
197
|
-
a2: Tuple[float, float, float],
|
|
198
|
-
a3: Tuple[float, float, float],
|
|
199
|
-
path: List[SymmetryPoint],
|
|
200
|
-
n_points: int = 50,
|
|
201
|
-
strategy: str = 'interpolate'):
|
|
202
|
-
"""Initialize a BrillouinZonePath."""
|
|
203
|
-
super().__init__(path, n_points, strategy)
|
|
204
|
-
self.a1 = np.array(a1)
|
|
205
|
-
self.a2 = np.array(a2)
|
|
206
|
-
self.a3 = np.array(a3)
|
|
207
|
-
self.dim = 3
|
|
208
|
-
|
|
209
|
-
@property
|
|
210
|
-
def _path(self) -> np.ndarray:
|
|
211
|
-
"""Return the path matrix."""
|
|
212
|
-
path = np.stack([p.point for p in self.path_points], axis=1)
|
|
213
|
-
return (path[0, :] * self.b1[:, None] + path[1, :] * self.b2[:, None] +
|
|
214
|
-
path[2, :] * self.b3[:, None])
|
|
215
|
-
|
|
216
|
-
@property
|
|
217
|
-
def betas(self) -> BlochVectors:
|
|
218
|
-
"""Return beta vector values and cumsum."""
|
|
219
|
-
beta_cs = np.cumsum(norm(np.diff(self._path, axis=1), axis=0))
|
|
220
|
-
beta_cs = np.pad(beta_cs, (1, 0), "constant")
|
|
221
|
-
if self.strat == 'interpolate':
|
|
222
|
-
return self._interpolate_beta(beta_cs, self._path, self.n_points)
|
|
223
|
-
raise NotImplementedError
|
|
224
|
-
|
|
225
|
-
@property
|
|
226
|
-
def b1(self) -> np.ndarray:
|
|
227
|
-
"""Return reciprocal lattice vector b1."""
|
|
228
|
-
return (2 * np.pi * np.cross(self.a2, self.a3) /
|
|
229
|
-
np.dot(self.a1, np.cross(self.a2, self.a3)))
|
|
230
|
-
|
|
231
|
-
@property
|
|
232
|
-
def b2(self) -> np.ndarray:
|
|
233
|
-
"""Return reciprocal lattice vector b2."""
|
|
234
|
-
return (2 * np.pi * np.cross(self.a3, self.a1) /
|
|
235
|
-
np.dot(self.a1, np.cross(self.a2, self.a3)))
|
|
236
|
-
|
|
237
|
-
@property
|
|
238
|
-
def b3(self) -> np.ndarray:
|
|
239
|
-
"""Return reciprocal lattice vector b3."""
|
|
240
|
-
return (2 * np.pi * np.cross(self.a1, self.a2) /
|
|
241
|
-
np.dot(self.a1, np.cross(self.a2, self.a3)))
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def BrillouinZonePath(
|
|
245
|
-
a1, *args, **kwargs
|
|
246
|
-
) -> Union[BrillouinZonePath1D, BrillouinZonePath2D, BrillouinZonePath3D]:
|
|
247
|
-
"""BrillouinZonePath factory."""
|
|
248
|
-
dim_obj = {
|
|
249
|
-
1: BrillouinZonePath1D,
|
|
250
|
-
2: BrillouinZonePath2D,
|
|
251
|
-
3: BrillouinZonePath3D
|
|
252
|
-
}
|
|
253
|
-
return dim_obj[len(a1)](a1, *args, **kwargs)
|
morpho.py-0.0.4/pyproject.toml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "morpho.py"
|
|
3
|
-
version = "0.0.4"
|
|
4
|
-
description = ""
|
|
5
|
-
license = "MIT"
|
|
6
|
-
authors = ["Tiago Vilela <tiagovla@gmail.com>"]
|
|
7
|
-
|
|
8
|
-
packages = [
|
|
9
|
-
{include = "morpho", from="."}
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
[tool.poetry.dependencies]
|
|
13
|
-
python = "^3.8"
|
|
14
|
-
numpy = "^1.20.1"
|
|
15
|
-
matplotlib = "^3.3.4"
|
|
16
|
-
scipy = "^1.6.1"
|
|
17
|
-
|
|
18
|
-
[tool.poetry.dev-dependencies]
|
|
19
|
-
pytest = "^5.2"
|
|
20
|
-
Sphinx = "^3.5.1"
|
|
21
|
-
sphinx-rtd-theme = "^0.5.1"
|
|
22
|
-
sphinx-autodoc-typehints = "^1.11.1"
|
|
23
|
-
|
|
24
|
-
[build-system]
|
|
25
|
-
requires = ["poetry-core>=1.0.0"]
|
|
26
|
-
build-backend = "poetry.core.masonry.api"
|
morpho.py-0.0.4/setup.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
from setuptools import setup
|
|
3
|
-
|
|
4
|
-
package_dir = \
|
|
5
|
-
{'': '.'}
|
|
6
|
-
|
|
7
|
-
packages = \
|
|
8
|
-
['morpho']
|
|
9
|
-
|
|
10
|
-
package_data = \
|
|
11
|
-
{'': ['*']}
|
|
12
|
-
|
|
13
|
-
install_requires = \
|
|
14
|
-
['matplotlib>=3.3.4,<4.0.0', 'numpy>=1.20.1,<2.0.0', 'scipy>=1.6.1,<2.0.0']
|
|
15
|
-
|
|
16
|
-
setup_kwargs = {
|
|
17
|
-
'name': 'morpho.py',
|
|
18
|
-
'version': '0.0.4',
|
|
19
|
-
'description': '',
|
|
20
|
-
'long_description': None,
|
|
21
|
-
'author': 'Tiago Vilela',
|
|
22
|
-
'author_email': 'tiagovla@gmail.com',
|
|
23
|
-
'maintainer': None,
|
|
24
|
-
'maintainer_email': None,
|
|
25
|
-
'url': None,
|
|
26
|
-
'package_dir': package_dir,
|
|
27
|
-
'packages': packages,
|
|
28
|
-
'package_data': package_data,
|
|
29
|
-
'install_requires': install_requires,
|
|
30
|
-
'python_requires': '>=3.8,<4.0',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setup(**setup_kwargs)
|
|
File without changes
|
|
File without changes
|