morpho.py 0.0.3__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.
@@ -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)
@@ -0,0 +1,265 @@
1
+ """Implement photonic crystal geometries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+ from dataclasses import dataclass
7
+ from typing import Callable, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+
11
+
12
+ class GeometryBase(ABC):
13
+ """Base Geometry class."""
14
+
15
+ def __init__(self, eps_rf: Optional[Callable], mu_rf: Optional[Callable]):
16
+ self.eps_rf: Optional[Callable] = eps_rf or None
17
+ self.mu_rf: Optional[Callable] = mu_rf or None
18
+ self._eps_r: np.ndarray
19
+ self._mu_r: np.ndarray
20
+
21
+ def overwrite(self, func: Callable):
22
+ """Overwrite decorator.
23
+
24
+ Parameters
25
+ ----------
26
+ func : Callable
27
+ func
28
+ """
29
+ if func.__name__ in ["eps_rf", "mu_rf"]:
30
+ setattr(self, func.__name__, func)
31
+ else:
32
+ raise Exception("Can only overwrite eps_rf or mu_rf.")
33
+
34
+
35
+ class Geometry1D(GeometryBase):
36
+ """Geometry class.
37
+
38
+ Parameters
39
+ ----------
40
+ a1 : Tuple[float, ...]
41
+ Direct a1 vector.
42
+ n1 : int
43
+ Number of divisions in the a1 vector direction.
44
+ eps_rf : Optional[Callable]
45
+ Permittivity matrix eps_r if directly supplied.
46
+ mu_rf : Optional[Callable]
47
+ Permeabillity matrix mu_r if directly supplied.
48
+ """
49
+
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
+ ):
57
+ super().__init__(eps_rf, mu_rf)
58
+ self.a1: np.ndarray = np.array(a1)
59
+ self.n1: int = n1
60
+ self._eps_r: np.ndarray = np.ones((n1,), dtype=complex)
61
+ self._mu_r: np.ndarray = np.ones((n1,), dtype=complex)
62
+
63
+ @property
64
+ def X(self) -> CartesianVectors1D:
65
+ """Return cartesian positions."""
66
+ P0 = np.arange(self.n1) / self.n1 - 0.5
67
+ x = P0 * self.a1[0]
68
+ return CartesianVectors1D(x)
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
+
84
+
85
+ class Geometry2D(GeometryBase):
86
+ """Geometry class.
87
+
88
+ Parameters
89
+ ----------
90
+ a1 : Tuple[float, ...]
91
+ Direct a1 vector.
92
+ a2 : Optional[Tuple[float, ...]]
93
+ Direct a2 vector.
94
+ n1 : int
95
+ Number of divisions in the a1 vector direction.
96
+ n2 : int
97
+ Number of divisions in the a2 vector direction.
98
+ eps_rf : Optional[Callable]
99
+ Permittivity matrix eps_r if directly supplied.
100
+ mu_rf : Optional[Callable]
101
+ Permeabillity matrix mu_r if directly supplied.
102
+ """
103
+
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
+ ):
113
+ super().__init__(eps_rf, mu_rf)
114
+ self.a1: np.ndarray = np.array(a1)
115
+ self.a2: np.ndarray = np.array(a2)
116
+ self.n1: int = n1
117
+ self.n2: int = n2
118
+ self._eps_r: np.ndarray = np.ones((n1, n2), dtype=complex)
119
+ self._mu_r: np.ndarray = np.ones((n1, n2), dtype=complex)
120
+
121
+ @property
122
+ def X(self) -> CartesianVectors2D:
123
+ """Return cartesian positions."""
124
+ P0, Q0 = np.meshgrid(
125
+ np.arange(self.n1) / self.n1 - 0.5,
126
+ np.arange(self.n2) / self.n2 - 0.5,
127
+ indexing="ij",
128
+ )
129
+
130
+ x = P0 * self.a1[0] + Q0 * self.a2[0]
131
+ y = P0 * self.a1[1] + Q0 * self.a2[1]
132
+
133
+ return CartesianVectors2D(x, y)
134
+
135
+ @property
136
+ def eps_r(self) -> np.ndarray:
137
+ """Return the relative permittivity matrice."""
138
+ if self.eps_rf:
139
+ self.eps_rf(eps_r=self._eps_r, x=self.X.x, y=self.X.y)
140
+ return self._eps_r
141
+
142
+ @property
143
+ def mu_r(self) -> np.ndarray:
144
+ """Return the relative permeabillity matrice."""
145
+ if self.mu_rf:
146
+ self.mu_rf(self._mu_r, x=self.X.x, y=self.X.y)
147
+ return self._mu_r
148
+
149
+
150
+ class Geometry3D(GeometryBase):
151
+ """Geometry class.
152
+
153
+ Parameters
154
+ ----------
155
+ a1 : Tuple[float, float, float]
156
+ Direct a1 vector.
157
+ a2 : Optional[Tuple[float, float, float]]
158
+ Direct a2 vector.
159
+ a3 : Optional[Tuple[float, float, float]]
160
+ Direct a3 vector.
161
+ n1 : int
162
+ Number of divisions in the a1 vector direction.
163
+ n2 : int
164
+ Number of divisions in the a2 vector direction.
165
+ n3 : int
166
+ Number of divisions in the a3 vector direction.
167
+ eps_rf : Optional[Callable]
168
+ Permittivity matrix function eps_rf if directly supplied.
169
+ mu_rf : Optional[Callable]
170
+ Permeabillity matrix function mu_rf if directly supplied.
171
+ """
172
+
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
+ ):
184
+ super().__init__(eps_rf, mu_rf)
185
+ self.a1: np.ndarray = np.array(a1)
186
+ self.a2: np.ndarray = np.array(a2)
187
+ self.a3: np.ndarray = np.array(a3)
188
+ self.n1: int = n1
189
+ self.n2: int = n2
190
+ self.n3: int = n3
191
+ self._eps_r: np.ndarray = np.ones((n1, n2, n3), dtype=complex)
192
+ self._mu_r: np.ndarray = np.ones((n1, n2, n3), dtype=complex)
193
+
194
+ @property
195
+ def X(self) -> CartesianVectors3D:
196
+ """Return cartesian positions."""
197
+ P0, Q0, R0 = np.meshgrid(
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",
202
+ )
203
+
204
+ x = P0 * self.a1[0] + Q0 * self.a2[0] + R0 * self.a3[0]
205
+ y = P0 * self.a1[1] + Q0 * self.a2[1] + R0 * self.a3[1]
206
+ z = P0 * self.a1[2] + Q0 * self.a2[2] + R0 * self.a3[2]
207
+
208
+ return CartesianVectors3D(x, y, z)
209
+
210
+ @property
211
+ def eps_r(self) -> np.ndarray:
212
+ """Return the relative permittivity matrice."""
213
+ if self.eps_rf:
214
+ self.eps_rf(eps_r=self._eps_r, x=self.X.x, y=self.X.y, z=self.X.z)
215
+ return self._eps_r
216
+
217
+ @property
218
+ def mu_r(self) -> np.ndarray:
219
+ """Return the relative permeabillity matrice."""
220
+ if self.mu_rf:
221
+ self.mu_rf(self._mu_r, x=self.X.x, y=self.X.y, z=self.X.z)
222
+ return self._mu_r
223
+
224
+
225
+ def Geometry(*args, **kwargs) -> Union[Geometry1D, Geometry2D, Geometry3D]:
226
+ """Geometry factory.
227
+
228
+ Parameters
229
+ ----------
230
+ args :
231
+ args
232
+ kwargs :
233
+ kwargs
234
+
235
+ Returns
236
+ -------
237
+ Union[Geometry1D, Geometry2D, Geometry3D]
238
+
239
+ """
240
+ dim_obj = {1: Geometry1D, 2: Geometry2D, 3: Geometry3D}
241
+ return dim_obj[len(args[0])](*args, **kwargs)
242
+
243
+
244
+ @dataclass
245
+ class CartesianVectors1D:
246
+ """Represent a data structure of cartesian coordinates."""
247
+
248
+ x: np.ndarray
249
+
250
+
251
+ @dataclass
252
+ class CartesianVectors2D:
253
+ """Represent a data structure of cartesian coordinates."""
254
+
255
+ x: np.ndarray
256
+ y: np.ndarray
257
+
258
+
259
+ @dataclass
260
+ class CartesianVectors3D:
261
+ """Represent a data structure of cartesian coordinates."""
262
+
263
+ x: np.ndarray
264
+ y: np.ndarray
265
+ z: np.ndarray
@@ -0,0 +1,230 @@
1
+ """Implement 3D/2D solvers."""
2
+
3
+ from typing import List, Union
4
+
5
+ import numpy as np
6
+ from scipy.linalg import eigh
7
+
8
+ from .brillouinzone import BrillouinZonePath1D as BZPath1D
9
+ from .brillouinzone import BrillouinZonePath2D as BZPath2D
10
+ from .brillouinzone import BrillouinZonePath3D as BZPath3D
11
+ from .geometry import Geometry1D, Geometry2D, Geometry3D
12
+ from .utils import convmat, harmonic_indices
13
+
14
+
15
+ class Solver1D:
16
+ """Implement a 1D PWE Solver.
17
+
18
+ Parameters
19
+ ----------
20
+ geometry : Geometry
21
+ geometry
22
+ path : BrillouinZonePath
23
+ A BrillouinZonePath object.
24
+ P : int
25
+ Number of terms in the direction of the reciprocal vector b1.
26
+ """
27
+
28
+ def __init__(
29
+ self, geometry: Geometry1D, path: BZPath1D, P: int = 1, pol: str = "TM"
30
+ ):
31
+ """Initialize the PWE Solver."""
32
+ self.geo = geometry
33
+ self.path = path
34
+ self.P = P
35
+ self.pol = pol
36
+
37
+ self.eps_rc: np.ndarray
38
+ self.mu_rc: np.ndarray
39
+ self.wn: List[np.ndarray] = []
40
+ self.modes: List[np.ndarray] = []
41
+
42
+ def run(self):
43
+ """Calculate the eigen-wavenumbers and modes."""
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)
67
+
68
+
69
+ class Solver2D:
70
+ """Implement a 2D PWE Solver.
71
+
72
+ Parameters
73
+ ----------
74
+ geometry : Geometry
75
+ geometry
76
+ path : BrillouinZonePath
77
+ A BrillouinZonePath object.
78
+ P : int
79
+ Number of terms in the direction of the reciprocal vector b1.
80
+ Q : int
81
+ Number of terms in the direction of the reciprocal vector b2.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ geometry: Geometry2D,
87
+ path: BZPath2D,
88
+ P: int = 1,
89
+ Q: int = 1,
90
+ pol: str = "TM",
91
+ ):
92
+ """Initialize the PWE Solver."""
93
+ self.geo = geometry
94
+ self.path = path
95
+ self.P, self.Q = P, Q
96
+ self.pol = pol
97
+
98
+ self.eps_rc: np.ndarray
99
+ self.mu_rc: np.ndarray
100
+ self.wn: List[np.ndarray] = []
101
+ self.modes: List[np.ndarray] = []
102
+
103
+ def run(self):
104
+ """Calculate the eigen-wavenumbers and modes."""
105
+ self.eps_rc = convmat(self.geo.eps_r, self.P, self.Q)
106
+ self.mu_rc = convmat(self.geo.mu_r, self.P, self.Q)
107
+
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)
114
+
115
+ p = harmonic_indices(self.P)
116
+ q = harmonic_indices(self.Q)
117
+
118
+ P0, Q0 = np.meshgrid(p, q, indexing="ij")
119
+ b1, b2 = self.path.b1, self.path.b2
120
+
121
+ G_k = P0.ravel(order="F") * b1[:, None] + Q0.ravel(order="F") * b2[:, None]
122
+ beta = self.path.betas.values
123
+
124
+ for col in range(beta.shape[1]):
125
+ k = beta[:, col, None] - G_k
126
+
127
+ KX = np.diag(k[0, :])
128
+ KY = np.diag(k[1, :])
129
+
130
+ if self.pol == "TM":
131
+ A = KX @ mu_rci @ KX + KY @ mu_rci @ KY
132
+ B = self.eps_rc
133
+ else:
134
+ A = KX @ eps_rci @ KX + KY @ eps_rci @ KY
135
+ B = self.mu_rc
136
+
137
+ D, V = eigh(A, B)
138
+
139
+ self.wn.append(np.sqrt(np.maximum(D, 0)))
140
+ self.modes.append(V)
141
+
142
+
143
+ class Solver3D:
144
+ """Implement a 3D PWE Solver.
145
+
146
+ Parameters
147
+ ----------
148
+ geometry : Geometry
149
+ geometry
150
+ path : BrillouinZonePath
151
+ A BrillouinZonePath object.
152
+ P : int
153
+ Number of terms in the direction of the reciprocal vector b1.
154
+ Q : int
155
+ Number of terms in the direction of the reciprocal vector b2.
156
+ R : int
157
+ Number of terms in the direction of the reciprocal vector b3.
158
+ """
159
+
160
+ def __init__(
161
+ self, geometry: Geometry3D, path: BZPath3D, P: int = 1, Q: int = 1, R: int = 1
162
+ ):
163
+ """Initialize the PWE Solver."""
164
+ self.geo = geometry
165
+ self.path = path
166
+ self.P, self.Q, self.R = P, Q, R
167
+
168
+ self.eps_rc: np.ndarray
169
+ self.mu_rc: np.ndarray
170
+ self.wn: List[np.ndarray] = []
171
+ self.modes: List[np.ndarray] = []
172
+
173
+ def run(self):
174
+ """Calculate the eigen-wavenumbers and modes."""
175
+ self.eps_rc = convmat(self.geo.eps_r, self.P, self.Q, self.R)
176
+ self.mu_rc = convmat(self.geo.mu_r, self.P, self.Q, self.R)
177
+
178
+ eps_rk = np.kron(np.eye(3), self.eps_rc)
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)
181
+
182
+ p = harmonic_indices(self.P)
183
+ q = harmonic_indices(self.Q)
184
+ r = harmonic_indices(self.R)
185
+
186
+ P0, Q0, R0 = np.meshgrid(p, q, r, indexing="ij")
187
+ b1, b2, b3 = self.path.b1, self.path.b2, self.path.b3
188
+
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
+ )
194
+
195
+ beta = self.path.betas.values
196
+ for col in range(beta.shape[1]):
197
+ k = beta[:, col, None] - G_k
198
+
199
+ KX = np.diag(k[0, :])
200
+ KY = np.diag(k[1, :])
201
+ KZ = np.diag(k[2, :])
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
+ )
209
+ A = K_V @ mu_rki @ K_V
210
+ B = eps_rk
211
+
212
+ D, V = eigh(A, B)
213
+
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])
225
+
226
+
227
+ def Solver(geometry, path, *args, **kwargs) -> Union[Solver1D, Solver2D, Solver3D]:
228
+ """Solver factory."""
229
+ dim_obj = {1: Solver1D, 2: Solver2D, 3: Solver3D}
230
+ return dim_obj[path.dim](geometry, path, *args, **kwargs)
@@ -0,0 +1,58 @@
1
+ """This module implements utilities."""
2
+
3
+ import numpy as np
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
+
17
+ def convmat(A: np.ndarray, P: int = 1, Q: int = 1, R: int = 1):
18
+ """Return a permittivity/permeability convolution matrix.
19
+
20
+ Parameters
21
+ ----------
22
+ A : np.ndarray
23
+ A 3D/2D/1D permittivity or permeability matrix.
24
+ P : int
25
+ Number of terms in the direction of the reciprocal vector T1.
26
+ Q : int
27
+ Number of terms in the direction of the reciprocal vector T2.
28
+ R : int
29
+ Number of terms in the direction of the reciprocal vector T3.
30
+ """
31
+ NH = P * Q * R
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.")
39
+ N1, N2, N3 = A.shape
40
+
41
+ p = harmonic_indices(P)
42
+ q = harmonic_indices(Q)
43
+ r = harmonic_indices(R)
44
+
45
+ A = np.fft.fftshift(np.fft.fftn(A) / A.size)
46
+
47
+ p_0, q_0, r_0 = N1 // 2, N2 // 2, N3 // 2
48
+
49
+ p_, q_, r_ = np.meshgrid(p, q, r, indexing="ij")
50
+ rr_, cc_ = np.mgrid[0:NH, 0:NH]
51
+ rr_i = np.unravel_index(rr_, p_.shape, order="F")
52
+ cc_i = np.unravel_index(cc_, p_.shape, order="F")
53
+
54
+ p_fft_ = p_[rr_i] - p_[cc_i] + p_0
55
+ q_fft_ = q_[rr_i] - q_[cc_i] + q_0
56
+ r_fft_ = r_[rr_i] - r_[cc_i] + r_0
57
+
58
+ return A[p_fft_, q_fft_, r_fft_]
@@ -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.3/PKG-INFO DELETED
@@ -1,13 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: morpho.py
3
- Version: 0.0.3
4
- Summary:
5
- Author: Tiago Vilela
6
- Author-email: tiagovla@gmail.com
7
- Requires-Python: >=3.8,<4.0
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.8
10
- Classifier: Programming Language :: Python :: 3.9
11
- Requires-Dist: matplotlib (>=3.3.4,<4.0.0)
12
- Requires-Dist: numpy (>=1.20.1,<2.0.0)
13
- Requires-Dist: scipy (>=1.6.1,<2.0.0)
@@ -1,5 +0,0 @@
1
- __version__ = "0.0.3"
2
-
3
- from morpho.brillouinzone import BrillouinZonePath, SymmetryPoint
4
- from morpho.geometry import Geometry
5
- from morpho.solver import Solver
@@ -1,80 +0,0 @@
1
- """This module implements classes related to the brillouinzone."""
2
-
3
- from typing import List, Optional, Tuple
4
-
5
- import numpy as np
6
- from numpy.linalg import norm
7
-
8
-
9
- class SymmetryPoint:
10
- """Model of a symmetry point at the brillouinzone."""
11
-
12
- def __init__(self, point: Tuple[float, ...], name: str):
13
- """Initialize a SymmetryPoint."""
14
- self.point = np.array(point)
15
- self.name = name
16
-
17
-
18
- class BrillouinZonePath:
19
- """Model of a path at the brillouinzone."""
20
-
21
- def __init__(
22
- self,
23
- path: List[SymmetryPoint],
24
- t1: Tuple[float, ...],
25
- t2: Optional[Tuple[float, ...]] = None,
26
- t3: Optional[Tuple[float, ...]] = None,
27
- n_points: int = 5,
28
- ):
29
- """Initialize a BrillouinZonePath."""
30
- self.path_points = path
31
- self.n_points = n_points
32
-
33
- self.dim = len(t1)
34
-
35
- self._t1 = np.pad(np.array(t1), (0, 3 - len(t1)), "constant")
36
- self._t2 = (np.pad(np.array(t2), (0, 3 - len(t2)), "constant")
37
- if t2 else np.array([0, 1, 0]))
38
- self._t3 = (np.pad(np.array(t3), (0, 3 - len(t1)), "constant")
39
- if t3 else np.array([0, 0, 1]))
40
-
41
- self._T1 = (2 * np.pi * np.cross(self._t2, self._t3) /
42
- np.dot(self._t1, np.cross(self._t2, self._t3)))
43
- self._T2 = (2 * np.pi * np.cross(self._t3, self._t1) /
44
- np.dot(self._t1, np.cross(self._t2, self._t3)))
45
- self._T3 = (2 * np.pi * np.cross(self._t1, self._t2) /
46
- np.dot(self._t1, np.cross(self._t2, self._t3)))
47
-
48
- self.beta_vec = None
49
- self.beta_vec_len = None
50
- self.symmetry_names = None
51
- self.symmetry_locations = None
52
- self._calculate_beta()
53
-
54
- def _calculate_beta(self):
55
- beta_path = np.stack(
56
- [
57
- np.pad(p.point, (0, 3 - len(p.point)), "constant")
58
- for p in self.path_points
59
- ],
60
- axis=1,
61
- )
62
- beta_path = (beta_path[0, :] * self._T1[:, None] +
63
- beta_path[1, :] * self._T2[:, None] +
64
- beta_path[2, :] * self._T3[:, None])
65
-
66
- beta_len = np.cumsum(
67
- norm(np.diff(beta_path, axis=1)[:self.dim], axis=0))
68
- beta_len = np.pad(beta_len, (1, 0), "constant")
69
- beta_len_interp = np.linspace(0, beta_len[-1], self.n_points)
70
- beta_ix = np.interp(beta_len_interp, beta_len,
71
- beta_path[0, :].flatten())
72
- beta_iy = np.interp(beta_len_interp, beta_len,
73
- beta_path[1, :].flatten())
74
- beta_iz = np.interp(beta_len_interp, beta_len,
75
- beta_path[2, :].flatten())
76
-
77
- self.beta_vec = np.vstack((beta_ix, beta_iy, beta_iz))[:self.dim, :]
78
- self.beta_vec_len = beta_len_interp
79
- self.symmetry_names = [p.name for p in self.path_points]
80
- self.symmetry_locations = beta_len
@@ -1,69 +0,0 @@
1
- """Implement photonic crystal geometries."""
2
-
3
- from typing import Callable, Optional, Tuple
4
-
5
- import matplotlib.pyplot as plt
6
- import numpy as np
7
-
8
-
9
- class Geometry:
10
- """Geometry class."""
11
-
12
- def __init__(
13
- self,
14
- t1: Tuple[float, float, float],
15
- t2: Tuple[float, float, float],
16
- t3: Tuple[float, float, float],
17
- Nx: int = 32,
18
- Ny: int = 32,
19
- Nz: int = 32,
20
- eps_r: Optional[np.ndarray] = None,
21
- mu_r: Optional[np.ndarray] = None,
22
- ):
23
- """Initialize the geometry obj."""
24
- self.t1 = np.array(t1).reshape(-1, 1)
25
- self.t2 = np.array(t2).reshape(-1, 1)
26
- self.t3 = np.array(t3).reshape(-1, 1)
27
-
28
- self.T1 = (2 * np.pi * np.cross(self.t2, self.t3, axis=0) /
29
- np.dot(t1, np.cross(self.t2, self.t3, axis=0)))
30
- self.T2 = (2 * np.pi * np.cross(self.t3, self.t1, axis=0) /
31
- np.dot(t1, np.cross(self.t2, self.t3, axis=0)))
32
- self.T3 = (2 * np.pi * np.cross(self.t1, self.t2, axis=0) /
33
- np.dot(t1, np.cross(self.t2, self.t3, axis=0)))
34
-
35
- self.eps_r = eps_r or np.ones((Nx, Ny, Nz), dtype=complex)
36
- self.mu_r = mu_r or np.ones((Nx, Ny, Nz), dtype=complex)
37
-
38
- P0, Q0, R0 = np.meshgrid(
39
- np.linspace(-0.5, 0.5, Nx),
40
- np.linspace(-0.5, 0.5, Ny),
41
- np.linspace(-0.5, 0.5, Nz),
42
- )
43
- self.x = P0 * self.t1[0] + Q0 * self.t2[0] + R0 * self.t3[0]
44
- self.y = P0 * self.t1[1] + Q0 * self.t2[1] + R0 * self.t3[1]
45
- self.z = P0 * self.t1[2] + Q0 * self.t2[2] + R0 * self.t3[2]
46
-
47
- self.epsr_f: Optional[Callable] = None
48
- self.mur_f: Optional[Callable] = None
49
-
50
- def setup(self):
51
- """Set the material properties."""
52
- if self.epsr_f:
53
- self.epsr_f()
54
- if self.mur_f:
55
- self.mur_f()
56
-
57
- def plot(self):
58
- """Plot ep_s and mu_r."""
59
- raise NotImplementedError
60
-
61
- def set_epsr_f(self, func: Callable):
62
- """Set decorator to set a eps_r profile."""
63
- self.epsr_f = func
64
- return func
65
-
66
- def set_mur_f(self, func: Callable):
67
- """Set decorator to set a mu_r profile."""
68
- self.mur_f = func
69
- return func
@@ -1,86 +0,0 @@
1
-
2
- """Implement the solver."""
3
-
4
- from typing import List
5
-
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
- from scipy.linalg import eigh
9
-
10
- from .brillouinzone import BrillouinZonePath as BZPath
11
- from .geometry import Geometry
12
- from .utils import convmat
13
-
14
-
15
- class Solver:
16
- """Implement the PWE solver."""
17
-
18
- def __init__(self,
19
- geometry: Geometry,
20
- path: BZPath,
21
- P: int = 1,
22
- Q: int = 1,
23
- R: int = 1):
24
- """Initialize the PWE Solver."""
25
- self.geo = geometry
26
- self.path = path
27
- self.P, self.Q, self.R = P, Q, R
28
- self.eps_rc: np.ndarray
29
- self.mu_rc: np.ndarray
30
-
31
- self.wn: List[np.ndarray] = []
32
- self.modes: List[np.ndarray] = []
33
-
34
- def run(self):
35
- """Run the simulation."""
36
- self.geo.setup()
37
- self.eps_rc = convmat(self.geo.eps_r, self.P, self.Q, self.R)
38
- self.mu_rc = convmat(self.geo.mu_r, self.P, self.Q, self.R)
39
-
40
- eps_rk = np.kron(np.eye(3), self.eps_rc)
41
- # mu_rk = np.kron(np.eye(3), self.eps_rc)
42
- # epsr_ki = np.kron(np.eye(3), np.linalg.inv(self.eps_rc))
43
- mu_rki = np.kron(np.eye(3), np.linalg.inv(self.mu_rc))
44
-
45
- p = np.arange(-(self.P // 2), self.P // 2 + 1)
46
- q = np.arange(-(self.Q // 2), self.Q // 2 + 1)
47
- r = np.arange(-(self.R // 2), self.R // 2 + 1)
48
-
49
- P0, Q0, R0 = np.meshgrid(p, q, r)
50
- T1, T2, T3 = self.geo.T1, self.geo.T2, self.geo.T3
51
-
52
- G_k = T1 * P0.flatten() + T2 * Q0.flatten() + T3 * R0.flatten()
53
- beta = self.path.beta_vec
54
- for col in range(beta.shape[1]):
55
- k = beta[:, col, None] - G_k
56
-
57
- KX = np.diag(k[0, :])
58
- KY = np.diag(k[1, :])
59
- KZ = np.diag(k[2, :])
60
- K_V = np.vstack((
61
- np.hstack((0 * KX, -KZ, KY)),
62
- np.hstack((KZ, 0 * KY, -KX)),
63
- np.hstack((-KY, KX, 0 * KZ)),
64
- ))
65
- A = K_V @ mu_rki @ K_V
66
- B = eps_rk
67
-
68
- D, V = eigh(A, B)
69
-
70
- self.wn.append(np.sqrt(-np.minimum(D, 0)))
71
- self.modes.append(V)
72
-
73
- def plot_bands(self):
74
- """Plot a bandgap diagram."""
75
- beta_len = self.path.beta_vec_len
76
- wn = np.vstack([d for d in self.wn])
77
- fig, ax = plt.subplots()
78
- ax.plot(beta_len, wn / (2 * np.pi), "k-")
79
- ax.set_xticklabels(self.path.symmetry_names)
80
- ax.set_xticks(self.path.symmetry_locations)
81
- ax.set_xlim(0, self.path.symmetry_locations[-1])
82
- ax.set_ylim(0, 2)
83
- ax.set_xlabel("Bloch Wave Vector $\\beta$")
84
- ax.set_ylabel("Frequency [$\\frac{a \\omega}{2\\pi c}$]")
85
- ax.grid(True)
86
- plt.show()
@@ -1,39 +0,0 @@
1
- """This module implements utilities."""
2
- import matplotlib.pyplot as plt
3
- import numpy as np
4
-
5
-
6
- def convmat(A: np.ndarray, P: int = 1, Q: int = 1, R: int = 1):
7
- """Create a convolution matrix."""
8
- NH = P * Q * R
9
- Nx, Ny, Nz = A.shape
10
-
11
- p = np.arange(-(P // 2), P//2 + 1)
12
- q = np.arange(-(Q // 2), Q//2 + 1)
13
- r = np.arange(-(R // 2), R//2 + 1)
14
-
15
- A = np.fft.fftshift(np.fft.fftn(A) / (Nx*Ny*Nz))
16
-
17
- p_0 = Nx // 2
18
- q_0 = Ny // 2
19
- r_0 = Nz // 2
20
-
21
- C = np.zeros((NH, NH), dtype=complex)
22
-
23
- for r_row in range(R):
24
- for q_row in range(Q):
25
- for p_row in range(P):
26
- row = r_row*Q*P + q_row*P + p_row
27
-
28
- for r_col in range(R):
29
- for q_col in range(Q):
30
- for p_col in range(P):
31
- col = r_col*Q*P + q_col*P + p_col
32
-
33
- p_fft = p[p_row] - p[p_col]
34
- q_fft = q[q_row] - q[q_col]
35
- r_fft = r[r_row] - r[r_col]
36
-
37
- C[row, col] = A[p_0 + p_fft, q_0 + q_fft,
38
- r_0 + r_fft]
39
- return C
@@ -1,25 +0,0 @@
1
- [tool.poetry]
2
- name = "morpho.py"
3
- version = "0.0.3"
4
- description = ""
5
- authors = ["Tiago Vilela <tiagovla@gmail.com>"]
6
-
7
- packages = [
8
- {include = "morpho", from="."}
9
- ]
10
-
11
- [tool.poetry.dependencies]
12
- python = "^3.8"
13
- numpy = "^1.20.1"
14
- matplotlib = "^3.3.4"
15
- scipy = "^1.6.1"
16
-
17
- [tool.poetry.dev-dependencies]
18
- pytest = "^5.2"
19
- Sphinx = "^3.5.1"
20
- sphinx-rtd-theme = "^0.5.1"
21
- sphinx-autodoc-typehints = "^1.11.1"
22
-
23
- [build-system]
24
- requires = ["poetry-core>=1.0.0"]
25
- build-backend = "poetry.core.masonry.api"
morpho.py-0.0.3/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.3',
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