treams
Advanced tools
+97
| Metadata-Version: 2.1 | ||
| Name: treams | ||
| Version: 0.4.2 | ||
| Summary: "T-matrix scattering code for nanophotonic computations" | ||
| Home-page: https://github.com/tfp-photonics/treams | ||
| Author: Dominik Beutel | ||
| Author-email: dominik.beutel@kit.edu | ||
| License: MIT | ||
| Platform: Linux | ||
| Platform: Windows | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: Intended Audience :: Science/Research | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Operating System :: POSIX :: Linux | ||
| Classifier: Natural Language :: English | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Programming Language :: Cython | ||
| Classifier: Programming Language :: Python :: Implementation :: CPython | ||
| Classifier: Topic :: Scientific/Engineering | ||
| Classifier: Topic :: Scientific/Engineering :: Astronomy | ||
| Classifier: Topic :: Scientific/Engineering :: Atmospheric Science | ||
| Classifier: Topic :: Scientific/Engineering :: Physics | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE | ||
| Requires-Dist: numpy | ||
| Requires-Dist: scipy >=1.6 | ||
| Provides-Extra: coverage | ||
| Requires-Dist: Cython ; extra == 'coverage' | ||
| Requires-Dist: pytest-cov ; extra == 'coverage' | ||
| Provides-Extra: docs | ||
| Requires-Dist: matplotlib ; extra == 'docs' | ||
| Requires-Dist: sphinx ; extra == 'docs' | ||
| Provides-Extra: io | ||
| Requires-Dist: h5py ; extra == 'io' | ||
| Provides-Extra: test | ||
| Requires-Dist: pytest ; extra == 'test' | ||
|  | ||
| [](https://pypi.org/project/treams) | ||
|  | ||
|  | ||
| [](https://tfp-photonics.github.io/treams) | ||
|  | ||
|  | ||
| [](https://htmlpreview.github.io/?https://github.com/tfp-photonics/treams/blob/htmlcov/index.html) | ||
| # treams | ||
| The package `treams` provides a framework to simplify computations of the | ||
| electromagnetic scattering of waves at finite and at periodic arrangements of particles | ||
| based on the T-matrix method. | ||
| ## Installation | ||
| ### Installation using pip | ||
| To install the package with pip, use | ||
| ```sh | ||
| pip install treams | ||
| ``` | ||
| If you're using the system wide installed version of python, you might consider the | ||
| ``--user`` option. | ||
| ## Documentation | ||
| The documentation can be found at https://tfp-photonics.github.io/treams. | ||
| ## Publications | ||
| When using this code please cite: | ||
| [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, treams - A T-matrix scattering code for nanophotonic computations, arXiv (preprint), 2309.03182 (2023).](https://doi.org/10.48550/arXiv.2309.03182) | ||
| Other relevant publications are | ||
| * [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, Unified Lattice Sums Accommodating Multiple Sublattices for Solutions of the Helmholtz Equation in Two and Three Dimensions, Phys. Rev. A 107, 013508 (2023).](https://doi.org/10.1103/PhysRevA.107.013508) | ||
| * [D. Beutel, P. Scott, M. Wegener, C. Rockstuhl, and I. Fernandez-Corbaton, Enhancing the Optical Rotation of Chiral Molecules Using Helicity Preserving All-Dielectric Metasurfaces, Appl. Phys. Lett. 118, 221108 (2021).](https://doi.org/10.1063/5.0050411) | ||
| * [D. Beutel, A. Groner, C. Rockstuhl, C. Rockstuhl, and I. Fernandez-Corbaton, Efficient Simulation of Biperiodic, Layered Structures Based on the T-Matrix Method, J. Opt. Soc. Am. B, JOSAB 38, 1782 (2021).](https://doi.org/10.1364/JOSAB.419645) | ||
| ## Features | ||
| * [x] T-matrix calculations using a spherical or cylindrical wave basis set | ||
| * [x] Calculations in helicity and parity (TE/TM) basis | ||
| * [x] Scattering from clusters of particles | ||
| * [x] Scattering from particles and clusters arranged in 3d-, 2d-, and 1d-lattices | ||
| * [x] Calculation of light propagation in stratified media | ||
| * [x] Band calculation in crystal structures |
+38
| treams-0.4.2.dist-info/RECORD,, | ||
| treams-0.4.2.dist-info/LICENSE,sha256=NYwAez5QkfLk8ZvAtw0WNxr81F1JmL696Mph7Ge5nPU,1071 | ||
| treams-0.4.2.dist-info/WHEEL,sha256=-h9omGws5mNk1JNCPjJw-Elug71_E6JM9ylV1yx54W8,109 | ||
| treams-0.4.2.dist-info/top_level.txt,sha256=pWrO0RbhgVZclsRKbrZfWxHvmP3r_v2K1wNT-7jiSq0,7 | ||
| treams-0.4.2.dist-info/METADATA,sha256=mK9hjHBiUGkGrnVBa8Aga_ZeJ0yqhDKT0EK9b1uLD3U,4515 | ||
| treams/misc.py,sha256=YSmsZzvVATXTnC_WBkGD19kayKXu99T6pYx-fturGpc,6531 | ||
| treams/config.cpython-310-darwin.so,sha256=RLVGNaR4QRlGvkLK4IlnZuq4rja8TVBIkBWzuFGQDu0,55184 | ||
| treams/_material.py,sha256=QIs2BUscmZ5fUwPgL6lLkVGDuSAE9uYFyqtm55qybfE,9634 | ||
| treams/coeffs.cpython-310-darwin.so,sha256=TzE5QCm-rcsYn6JgdsmFa04HIl7i_wv3WmGvmRAZan4,142256 | ||
| treams/cw.cpython-310-darwin.so,sha256=my_Tn7mAxg0fz40FkU54za67ptktMdUTa1waf-iJLoE,180056 | ||
| treams/util.py,sha256=4ZqBi223ET6jQKTNSm9jE51sZO561g8SGDR57EQ72c0,40927 | ||
| treams/io.py,sha256=brdDjYWTbqfPeYxHahai7ZRiVEUC7hwRFujyIQx8Zyc,14886 | ||
| treams/__init__.py,sha256=dMU-fsKHJjPyQ0lfdiuV9IPq7b-sJMaXgDNqJT9k5Tk,1736 | ||
| treams/pw.cpython-310-darwin.so,sha256=qn-LBF8OGtXf9k5v9IxFIDI_kibZhU3ACYTTa7F75VA,137048 | ||
| treams/_operators.py,sha256=8Wx2YZ8JgylCtx2JM9dCddbb4AL4aLei79PJIRenT0U,75357 | ||
| treams/_core.py,sha256=ehMwhmG-SMnyk2mIrp-NnwzZ0Rob52iQtFKGYh70ZFU,49997 | ||
| treams/sw.cpython-310-darwin.so,sha256=RdsJbZ6fZPmINeJRyWiM71SGCfOT_arDFGA71Rvt29o,216552 | ||
| treams/_lattice.py,sha256=Ki1zhPhJYIT2-BzmG2SRznVEzxjyTqcxpbLbBX0NEfQ,18668 | ||
| treams/_tmatrix.py,sha256=BTbECf72vES6xJcbFfI-sDHreP3YDFpDsOGGhczmVvw,35187 | ||
| treams/ebcm.py,sha256=dr6q_IBFDST3dMS8mCuZz2_lFNZ7oUvK4gvE32qYIuI,2952 | ||
| treams/_smatrix.py,sha256=LIhVS1kIK6j4bzqwhuYI7bEw9suZT4HSBCEw_rK6Nr8,23001 | ||
| treams/special/_wigner3j.cpython-310-darwin.so,sha256=VuQ43kMqvZoA4GkVcW1xsGoVWneolWPwp4ZoMOYPJsM,57792 | ||
| treams/special/_integrals.cpython-310-darwin.so,sha256=S-VjutQTIyuGHkCzlqSNKV3bDU58HgK0iS_f9JN3lfs,77008 | ||
| treams/special/_wignerd.cpython-310-darwin.so,sha256=WpPJOUSr0fpRs7stDgcFzl-YlXBj_82yQ7hvaThBoEY,76336 | ||
| treams/special/__init__.py,sha256=CLqs0pI4KSFjcC_IlmYi5VXpZSzBFj6l9X9TuzCvHSY,6219 | ||
| treams/special/_gufuncs.cpython-310-darwin.so,sha256=FB4oBU94cds8gRo35ZM2EShAly4xBilW0HRDJlfb7OU,110592 | ||
| treams/special/_coord.cpython-310-darwin.so,sha256=Z85llsZUB3ouFIXgq1EEqzOv2BISKOqHmIWzEkepXFw,75584 | ||
| treams/special/_waves.cpython-310-darwin.so,sha256=6Ha2OuNiVswWsP3Mi7XakuGSaj_epYyThrTcEgcaRRM,132144 | ||
| treams/special/_misc.cpython-310-darwin.so,sha256=-sFuOyb4q79GuUDmADBSeubNTHIWqpwfkvKm46spD3g,59848 | ||
| treams/special/_ufuncs.cpython-310-darwin.so,sha256=8y-G9S4wS9qiBxW2vk3PJ3Jp_H8zMxGW52RF4ZE1bAk,110288 | ||
| treams/special/_bessel.cpython-310-darwin.so,sha256=8xnE6JC76bZHIR6J3ziLgs_sc3LUBZk1DEi1YG28xLY,57664 | ||
| treams/special/cython_special.cpython-310-darwin.so,sha256=fHv1oyyp7veocUxxzqweVubG_ugO6r69BRO-7x8lUpw,391992 | ||
| treams/lattice/cython_lattice.cpython-310-darwin.so,sha256=DKiU9hqq6YpRO5zi2YbS20zh5f05qHNXuTpIW83CJYw,374008 | ||
| treams/lattice/__init__.py,sha256=9pJF4GG2KWRDbqGAeoS7m_o2alEfo1epmcY4HEEaWdY,7467 | ||
| treams/lattice/_gufuncs.cpython-310-darwin.so,sha256=L19bpF8hlmYTlXAgKzJqCup7v2XIhMdXTCiIHS__smY,236416 | ||
| treams/lattice/_dsum.cpython-310-darwin.so,sha256=hed6aygAf6T4QT6yV6O4Ls5eqYLuacCVTLbFxuBnBNM,185048 | ||
| treams/lattice/_misc.cpython-310-darwin.so,sha256=5P2LZlGUeLOm9MsoE16nSQPuwZ4Mt12I76qk0RlqWj4,262712 | ||
| treams/lattice/_esum.cpython-310-darwin.so,sha256=UcK27YjvvuXuIIY0KdcX5oCg3pUaY85-gVQfbWoipz8,289400 |
| treams |
| """TREAMS: T-Matrix scattering code for nanophotonic computations. | ||
| .. currentmodule:: treams | ||
| Classes | ||
| ======= | ||
| The top-level classes and functions allow a high-level access to the functionality. | ||
| Basis sets | ||
| ---------- | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| CylindricalWaveBasis | ||
| PlaneWaveBasisByUnitVector | ||
| PlaneWaveBasisByComp | ||
| SphericalWaveBasis | ||
| Matrices and Arrays | ||
| ------------------- | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| PhysicsArray | ||
| SMatrix | ||
| SMatrices | ||
| TMatrix | ||
| TMatrixC | ||
| Other | ||
| ----- | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| Lattice | ||
| Material | ||
| Functions | ||
| ========= | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| bfield | ||
| changepoltype | ||
| chirality_density | ||
| dfield | ||
| efield | ||
| expand | ||
| expandlattice | ||
| hfield | ||
| permute | ||
| plane_wave | ||
| rotate | ||
| translate | ||
| """ | ||
| from treams._core import ( # noqa: F401 | ||
| CylindricalWaveBasis, | ||
| PhysicsArray, | ||
| PlaneWaveBasisByComp, | ||
| PlaneWaveBasisByUnitVector, | ||
| SphericalWaveBasis, | ||
| ) | ||
| from treams._lattice import Lattice, WaveVector # noqa: F401 | ||
| from treams._material import Material # noqa: F401 | ||
| from treams._operators import ( # noqa: F401 | ||
| BField, | ||
| ChangePoltype, | ||
| DField, | ||
| EField, | ||
| Expand, | ||
| ExpandLattice, | ||
| FField, | ||
| GField, | ||
| HField, | ||
| Permute, | ||
| Rotate, | ||
| Translate, | ||
| bfield, | ||
| changepoltype, | ||
| dfield, | ||
| efield, | ||
| expand, | ||
| expandlattice, | ||
| ffield, | ||
| gfield, | ||
| hfield, | ||
| permute, | ||
| rotate, | ||
| translate, | ||
| ) | ||
| from treams._smatrix import ( # noqa: F401 | ||
| SMatrices, | ||
| SMatrix, | ||
| chirality_density, | ||
| poynting_avg_z, | ||
| ) | ||
| from treams._tmatrix import ( # noqa: F401 | ||
| TMatrix, | ||
| TMatrixC, | ||
| cylindrical_wave, | ||
| plane_wave, | ||
| plane_wave_angle, | ||
| spherical_wave, | ||
| ) |
+1297
| """Basis sets and core array functionalities.""" | ||
| import abc | ||
| from collections import namedtuple | ||
| import numpy as np | ||
| import treams._operators as op | ||
| import treams.lattice as la | ||
| from treams import util | ||
| from treams._lattice import Lattice, WaveVector | ||
| from treams._material import Material | ||
| class BasisSet(util.OrderedSet, metaclass=abc.ABCMeta): | ||
| """Basis set base class. | ||
| It is the base class for all basis sets used. They are expected to be an ordered | ||
| sequence of the modes, that are included in a expansion. Basis sets are expected to | ||
| be immutable. | ||
| """ | ||
| _names = () | ||
| """Names of the relevant parameters""" | ||
| def __repr__(self): | ||
| """String representation. | ||
| Automatically generated when the attribute ``_names`` is defined. | ||
| Returns: | ||
| str | ||
| """ | ||
| string = ",\n ".join(f"{name}={i}" for name, i in zip(self._names, self[()])) | ||
| return f"{self.__class__.__name__}(\n {string},\n)" | ||
| def __len__(self): | ||
| """Number of modes.""" | ||
| return len(self.pol) | ||
| @classmethod | ||
| @abc.abstractmethod | ||
| def default(cls, *args, **kwargs): | ||
| """Construct a default basis from parameters. | ||
| Construct a basis set in a default order by giving few parameters. | ||
| """ | ||
| raise NotImplementedError | ||
| class SphericalWaveBasis(BasisSet): | ||
| r"""Basis of spherical waves. | ||
| Functions of the spherical wave basis are defined by their angular momentum ``l``, | ||
| its projection onto the z-axis ``m``, and the polarization ``pol``. If the basis | ||
| is defined with respect to a single origin it is referred to as "global", if it | ||
| contains multiple origins it is referred to as "local". In a local basis an | ||
| additional position index ``pidx`` is used to link the modes to one of the | ||
| specified ``positions``. | ||
| For spherical waves there exist multiple :ref:`params:Polarizations` and they | ||
| can be separated into incident and scattered fields. Depending on these combinations | ||
| the basis modes refer to one of the functions :func:`~treams.special.vsw_A`, | ||
| :func:`~treams.special.vsw_rA`, :func:`~treams.special.vsw_M`, | ||
| :func:`~treams.special.vsw_rM`, :func:`~treams.special.vsw_N`, or | ||
| :func:`~treams.special.vsw_rN`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``l``, ``m``, and | ||
| ``pol`` or ``pidx``, ``l``, ``m``, and``pol``. | ||
| positions (array-like, optional): The positions of the origins for the specified | ||
| modes. Defaults to ``[[0, 0, 0]]``. | ||
| Attributes: | ||
| pidx (array-like): Integer referring to a row in :attr:`positions`. | ||
| l (array-like): Angular momentum as an integer :math:`l > 0` | ||
| m (array-like): Angular momentum projection onto the z-axis, it is an integer | ||
| with :math:`m \leq |l|` | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| _names = ("pidx", "l", "m", "pol") | ||
| def __init__(self, modes, positions=None): | ||
| """Initalization.""" | ||
| tmp = [] | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| pidx = [] | ||
| l = [] # noqa: E741 | ||
| m = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 3: | ||
| l, m, pol = (*zip(*modes),) | ||
| pidx = np.zeros_like(l) | ||
| elif len(modes[0]) == 4: | ||
| pidx, l, m, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| if positions is None: | ||
| positions = np.zeros((1, 3)) | ||
| positions = np.array(positions, float) | ||
| if positions.ndim == 1: | ||
| positions = positions[None, :] | ||
| if positions.ndim != 2 or positions.shape[1] != 3: | ||
| raise ValueError(f"invalid shape of positions {positions.shape}") | ||
| self.pidx, self.l, self.m, self.pol = [ | ||
| np.array(i, int) for i in (pidx, l, m, pol) | ||
| ] | ||
| for i, j in ((self.pidx, pidx), (self.l, l), (self.m, m), (self.pol, pol)): | ||
| i.flags.writeable = False | ||
| if np.any(i != j): | ||
| raise ValueError("parameters must be integer") | ||
| if np.any(self.l < 1): | ||
| raise ValueError("'l' must be a strictly positive integer") | ||
| if np.any(self.l < np.abs(self.m)): | ||
| raise ValueError("'|m|' cannot be larger than 'l'") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| if np.any(self.pidx >= len(positions)): | ||
| raise ValueError("undefined position is indexed") | ||
| self._positions = positions | ||
| self._positions.flags.writeable = False | ||
| self.lattice = self.kpar = None | ||
| @property | ||
| def positions(self): | ||
| """Positions of the modes' origins. | ||
| The positions are an immutable (N, 3)-array. Each row corresponds to a point in | ||
| the three-dimensional Cartesian space. | ||
| """ | ||
| return self._positions | ||
| def __repr__(self): | ||
| """String representation.""" | ||
| positions = "positions=" + str(self.positions).replace("\n", ",") | ||
| return f"{super().__repr__()[:-1]} {positions},\n)" | ||
| @property | ||
| def isglobal(self): | ||
| """Basis is defined with respect to a single (global) origin. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return len(self) == 0 or np.all(self.pidx == self.pidx[0]) | ||
| def __getattr__(self, key): | ||
| dct = {"l": "l", "m": "m", "p": "pidx", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| Alternatively, the string "plmp", "lmp", or "lm" can be used to access only a | ||
| subset of :attr:`pidx`, :attr:`l`, :attr:`m`, and :attr:`pol`. | ||
| """ | ||
| res = self.pidx[idx], self.l[idx], self.m[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or ( | ||
| isinstance(idx, tuple) and len(idx) == 0 | ||
| ): | ||
| return res | ||
| return type(self)(zip(*res), self.positions) | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same order | ||
| and the specified origin :attr:`positions` are equal. | ||
| """ | ||
| try: | ||
| return self is other or ( | ||
| np.array_equal(self.pidx, other.pidx) | ||
| and np.array_equal(self.l, other.l) | ||
| and np.array_equal(self.m, other.m) | ||
| and np.array_equal(self.pol, other.pol) | ||
| and np.array_equal(self.positions, other.positions) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| @classmethod | ||
| def default(cls, lmax, nmax=1, positions=None): | ||
| """Default basis for the given maximal multipolar order. | ||
| The default order contains separate blocks for each position index which are in | ||
| ascending order. Within each block the modes are sorted by angular momentum | ||
| :math:`l`, with the lowest angular momentum coming first. For each angular | ||
| momentum its z-projection is in ascending order from :math:`m = -l` to | ||
| :math:`m = l`. Finally, the polarization index is the fastest changing index | ||
| which iterates between 1 and 0. | ||
| Example: | ||
| >>> SphericalWaveBasis.default(2) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2], | ||
| m=[-1 -1 0 0 1 1 -2 -2 -1 -1 0 0 1 1 2 2], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| >>> SphericalWaveBasis.default(1, 2, [[0, 0, 1.], [0, 0, -1.]]) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 1 1 1 1 1 1], | ||
| l=[1 1 1 1 1 1 1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[ 0. 0. 1.], [ 0. 0. -1.]], | ||
| ) | ||
| Args: | ||
| lmax (int): Maximal multipolar order. | ||
| nmax (int, optional): Number of positions, defaults to 1. | ||
| positions (array-like, optional): Positions of the origins. | ||
| """ | ||
| modes = [ | ||
| [n, l, m, p] | ||
| for n in range(0, nmax) | ||
| for l in range(1, lmax + 1) # noqa: E741 | ||
| for m in range(-l, l + 1) | ||
| for p in range(1, -1, -1) | ||
| ] | ||
| return cls(modes, positions=positions) | ||
| @classmethod | ||
| def ebcm(cls, lmax, nmax=1, mmax=-1, positions=None): | ||
| """Order of modes suited for ECBM. | ||
| In comparison to :meth:`default` this order prioritises blocks of the | ||
| z-projection of the angular momentum :math:`m` over the angular momentum | ||
| :math:`l`. This is useful to get block-diagonal matrices for the extended | ||
| boundary condition method (EBCM). | ||
| Example: | ||
| >>> SphericalWaveBasis.ebcm(2) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| l=[2 2 1 1 2 2 1 1 2 2 1 1 2 2 2 2], | ||
| m=[-2 -2 -1 -1 -1 -1 0 0 0 0 1 1 1 1 2 2], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| Args: | ||
| lmax (int): Maximal multipolar order. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| mmax (int, optional): Maximal value of `|m|`. If ommitted or set to -1 it | ||
| is taken equal to `lmax`. | ||
| positions (array-like, optional): Positions of the origins. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| mmax = lmax if mmax == -1 else mmax | ||
| modes = [ | ||
| [n, l, m, p] | ||
| for n in range(0, nmax) | ||
| for m in range(-mmax, mmax + 1) | ||
| for l in range(max(abs(m), 1), lmax + 1) # noqa: E741 | ||
| for p in range(1, -1, -1) | ||
| ] | ||
| return cls(modes, positions=positions) | ||
| @staticmethod | ||
| def defaultlmax(dim, nmax=1): | ||
| """Calculate the default mode order for a given length. | ||
| Given the dimension of the T-matrix return the estimated maximal value of `l`. | ||
| This is the inverse of :meth:`defaultdim`. A value of zero is allowed for empty | ||
| T-matrices. | ||
| Example: | ||
| >>> SphericalWaveBasis.defaultlmax(len(SphericalWaveBasis.default(3))) | ||
| 3 | ||
| Args: | ||
| dim (int): Dimension of the T-matrix, respectively number of modes. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| Returns: | ||
| int | ||
| """ | ||
| res = np.sqrt(1 + dim * 0.5 / nmax) - 1 | ||
| res_int = int(np.rint(res)) | ||
| if np.abs(res - res_int) > 1e-8 * np.maximum(np.abs(res), np.abs(res_int)): | ||
| raise ValueError("cannot estimate the default lmax") | ||
| return res_int | ||
| @staticmethod | ||
| def defaultdim(lmax, nmax=1): | ||
| """Default number of modes for a given mulipolar order. | ||
| Given the maximal value of `l` return the size of the corresponding T-matrix. | ||
| This is the inverse of :meth:`defaultlmax`. A value of zero is allowed. | ||
| Args: | ||
| lmax (int): Maximal multipolar order | ||
| nmax (int, optional): Number of particles, defaults to `1` | ||
| Returns: | ||
| int | ||
| """ | ||
| # zero is allowed and won't give an error | ||
| if lmax < 0 or nmax < 0: | ||
| raise ValueError("maximal order must be positive") | ||
| return 2 * lmax * (lmax + 2) * nmax | ||
| @classmethod | ||
| def _from_iterable(cls, it, positions=None): | ||
| if isinstance(cls, SphericalWaveBasis): | ||
| positions = cls.positions if positions is None else positions | ||
| cls = type(cls) | ||
| obj = cls(it, positions=positions) | ||
| return obj | ||
| class CylindricalWaveBasis(BasisSet): | ||
| r"""Basis of cylindrical waves. | ||
| Functions of the cylindrical wave basis are defined by the z-components of the wave | ||
| vector ``kz`` and the angular momentum ``m`` as well as the polarization ``pol``. | ||
| If the basis is defined with respect to a single origin it is referred to as | ||
| "global", if it contains multiple origins it is referred to as "local". In a local | ||
| basis an additional position index ``pidx`` is used to link the modes to one of the | ||
| specified ``positions``. | ||
| For cylindrical waves there exist multiple :ref:`params:Polarizations` and | ||
| they can be separated into incident and scattered fields. Depending on these | ||
| combinations the basis modes refer to one of the functions | ||
| :func:`~treams.special.vcw_A`, :func:`~treams.special.vcw_rA`, | ||
| :func:`~treams.special.vcw_M`, :func:`~treams.special.vcw_rM`, | ||
| :func:`~treams.special.vcw_N`, or :func:`~treams.special.vcw_rN`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``kz``, ``m``, and | ||
| ``pol`` or ``pidx``, ``kz``, ``m``, and``pol``. | ||
| positions (array-like, optional): The positions of the origins for the specified | ||
| modes. Defaults to ``[[0, 0, 0]]``. | ||
| Attributes: | ||
| pidx (array-like): Integer referring to a row in :attr:`positions`. | ||
| kz (array-like): Real valued z-component of the wave vector. | ||
| m (array-like): Integer angular momentum projection onto the z-axis. | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| _names = ("pidx", "kz", "m", "pol") | ||
| def __init__(self, modes, positions=None): | ||
| """Initalization.""" | ||
| tmp = [] | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| pidx = [] | ||
| kz = [] | ||
| m = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 3: | ||
| kz, m, pol = (*zip(*modes),) | ||
| pidx = np.zeros_like(m) | ||
| elif len(modes[0]) == 4: | ||
| pidx, kz, m, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| if positions is None: | ||
| positions = np.zeros((1, 3)) | ||
| positions = np.array(positions, float) | ||
| if positions.ndim == 1: | ||
| positions = positions[None, :] | ||
| if positions.ndim != 2 or positions.shape[1] != 3: | ||
| raise ValueError(f"invalid shape of positions {positions.shape}") | ||
| self.pidx, self.m, self.pol = [np.array(i, int) for i in (pidx, m, pol)] | ||
| self.kz = np.array(kz, float) | ||
| self.kz.flags.writeable = False | ||
| for i, j in ((self.pidx, pidx), (self.m, m), (self.pol, pol)): | ||
| i.flags.writeable = False | ||
| if np.any(i != j): | ||
| raise ValueError("parameters must be integer") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| if np.any(self.pidx >= len(positions)): | ||
| raise ValueError("undefined position is indexed") | ||
| self._positions = positions | ||
| self._positions.flags.writeable = False | ||
| self.lattice = self.kpar = None | ||
| if len(self.kz) > 0 and np.all(self.kz == self.kz[0]): | ||
| self.kpar = WaveVector(self.kz[0]) | ||
| @property | ||
| def positions(self): | ||
| """Positions of the modes' origins. | ||
| The positions are an immutable (N, 3)-array. Each row corresponds to a point in | ||
| the three-dimensional Cartesian space. | ||
| """ | ||
| return self._positions | ||
| def __repr__(self): | ||
| """String representation.""" | ||
| positions = "positions=" + str(self.positions).replace("\n", ",") | ||
| return f"{super().__repr__()[:-1]} {positions},\n)" | ||
| @property | ||
| def isglobal(self): | ||
| """Basis is defined with respect to a single (global) origin. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return len(self) == 0 or np.all(self.pidx == self.pidx[0]) | ||
| def __getattr__(self, key): | ||
| dct = {"z": "kz", "m": "m", "p": "pidx", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| Alternatively, the string "pkzmp", "kzmp", or "kzm" can be used to access only a | ||
| subset of :attr:`pidx`, :attr:`kz`, :attr:`m`, and :attr:`pol`. | ||
| """ | ||
| res = self.pidx[idx], self.kz[idx], self.m[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or (isinstance(idx, tuple) and idx == ()): | ||
| return res | ||
| return type(self)(zip(*res), self.positions) | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same order | ||
| and the specified origin :attr:`positions` are equal. | ||
| """ | ||
| try: | ||
| return self is other or ( | ||
| np.array_equal(self.pidx, other.pidx) | ||
| and np.array_equal(self.kz, other.kz) | ||
| and np.array_equal(self.m, other.m) | ||
| and np.array_equal(self.pol, other.pol) | ||
| and np.array_equal(self.positions, other.positions) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| @classmethod | ||
| def default(cls, kzs, mmax, nmax=1, positions=None): | ||
| """Default basis for the given z-components of wave vector and angular momentum. | ||
| The default order contains separate blocks for each position index which are in | ||
| ascending order. Within each block the modes are sorted by the z-component of | ||
| the wave vector :math:`k_z`. For each of those values the z-projection of the | ||
| angular momentum is placed in ascending order. Finally, the polarization index | ||
| is the fastest changing index which iterates between 1 and 0. | ||
| Example: | ||
| >>> CylindricalWaveBasis.default([-0.5, 0.5], 1) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0], | ||
| kz=[-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 0.5 0.5 0.5 0.5 0.5 0.5], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| >>> CylindricalWaveBasis.default([0], 1, 2, [[1., 0, 0], [-1., 0, 0]]) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 1 1 1 1 1 1], | ||
| kz=[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[ 1. 0. 0.], [-1. 0. 0.]], | ||
| ) | ||
| Args: | ||
| kzs (array-like, float): Maximal multipolar order. | ||
| mmax (int): Maximal value of the angular momentum z-component. | ||
| nmax (int, optional): Number of positions, defaults to 1. | ||
| positions (array-like, optional): Positions of the origins. | ||
| """ | ||
| kzs = np.atleast_1d(kzs) | ||
| if kzs.ndim > 1: | ||
| raise ValueError(f"kzs has dimension larger than one: '{kzs.ndim}'") | ||
| modes = [ | ||
| [n, kz, m, p] | ||
| for n in range(nmax) | ||
| for kz in kzs | ||
| for m in range(-mmax, mmax + 1) | ||
| for p in range(1, -1, -1) | ||
| ] | ||
| return cls(modes, positions=positions) | ||
| @classmethod | ||
| def diffr_orders(cls, kz, mmax, lattice, bmax, nmax=1, positions=None): | ||
| """Create a basis set for a system periodic in the z-direction. | ||
| Example: | ||
| >>> CylindricalWaveBasis.diffr_orders(0.1, 1, 2 * np.pi, 1) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| kz=[-0.9 -0.9 -0.9 -0.9 -0.9 -0.9 0.1 0.1 0.1 0.1 0.1 0.1 1.1 1.1 | ||
| 1.1 1.1 1.1 1.1], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| Args: | ||
| kz (float): Wave vector z-component. Ideally it is in the first Brillouin | ||
| zone (use :func:`misc.firstbrillouin1d`). | ||
| mmax (int): Maximal value for the z-component of the angular momentum. | ||
| lattice (:class:`treams.Lattice` or float): Lattice definition or pitch. | ||
| bmax (float): Maximal change of the z-component of the wave vector. So, | ||
| this defines a maximal momentum transfer from the given value `kz`. | ||
| nmax (int, optional): Number of positions. | ||
| positions (array-like, optional): Positions of the origins. | ||
| """ | ||
| lattice = Lattice(lattice) | ||
| lattice_z = Lattice(lattice, "z") | ||
| nkz = np.floor(np.abs(bmax / lattice_z.reciprocal)) | ||
| kzs = kz + np.arange(-nkz, nkz + 1) * lattice_z.reciprocal | ||
| res = cls.default(kzs, mmax, nmax, positions=positions) | ||
| res.lattice = lattice | ||
| res.kpar = WaveVector(kz) | ||
| return res | ||
| @classmethod | ||
| def _from_iterable(cls, it, positions=None): | ||
| if isinstance(cls, CylindricalWaveBasis): | ||
| positions = cls.positions if positions is None else positions | ||
| lattice = cls.lattice | ||
| kpar = cls.kpar | ||
| cls = type(cls) | ||
| else: | ||
| lattice = kpar = None | ||
| obj = cls(it, positions=positions) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| @staticmethod | ||
| def defaultmmax(dim, nkz=1, nmax=1): | ||
| """Calculate the default mode order for a given length. | ||
| Given the dimension of the T-matrix return the estimated maximal value of `m`. | ||
| This is the inverse of :meth:`defaultdim`. A value of zero is allowed for empty | ||
| T-matrices. | ||
| Example: | ||
| >>> CylindricalWaveBasis.defaultmmax(len(CylindricalWaveBasis.default([0], 2)), 1) | ||
| 2 | ||
| Args: | ||
| dim (int): Dimension of the T-matrix, respectively number of modes | ||
| nkz (int, optional): Number of z-components of the wave vector. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| Returns: | ||
| int | ||
| """ | ||
| if dim % (2 * nkz * nmax) != 0: | ||
| raise ValueError("cannot estimate the default mmax") | ||
| dim = dim // (2 * nkz * nmax) | ||
| if (dim - 1) % 2 != 0: | ||
| raise ValueError("cannot estimate the default mmax") | ||
| return (dim - 1) // 2 | ||
| @staticmethod | ||
| def defaultdim(nkz, mmax, nmax=1): | ||
| """Default number of modes for a given mulipolar order. | ||
| Given the maximal value of `l` return the size of the corresponding T-matrix. | ||
| This is the inverse of :meth:`defaultlmax`. A value of zero is allowed. | ||
| Args: | ||
| nkz (int): Number of z-components of the wave vector. | ||
| mmax (int): Maximal value of the angular momentum's z-component. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| Returns: | ||
| int | ||
| """ | ||
| if nkz < 0 or mmax < 0: | ||
| raise ValueError("maximal order must be positive") | ||
| return (2 * mmax + 1) * nkz * 2 * nmax | ||
| class PlaneWaveBasis(BasisSet): | ||
| """Plane wave basis parent class.""" | ||
| isglobal = True | ||
| class PlaneWaveBasisByUnitVector(PlaneWaveBasis): | ||
| """Plane wave basis. | ||
| A plane wave basis is defined by a collection of wave vectors specified by the | ||
| Cartesian wave vector components ``qx``, ``qy``, and ``qz`` normalized to | ||
| :math:`q_x^2 + q_y^2 + q_z^2 = 1` and the polarizations ``pol``. | ||
| For plane waves there exist multiple :ref:`params:Polarizations`, such that | ||
| these modes can refer to either :func:`~treams.special.vpw_A` or | ||
| :func:`~treams.special.vpw_M` and :func:`~treams.special.vpw_N`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``qx``, ``qy``, | ||
| ``qz``, and ``pol``. | ||
| Attributes: | ||
| qx (array-like): X-component of the normalized wave vector. | ||
| qy (array-like): Y-component of the normalized wave vector. | ||
| qz (array-like): Z-component of the normalized wave vector. | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| _names = ("qx", "qy", "qz", "pol") | ||
| """A plane wave basis is always global.""" | ||
| def __init__(self, modes): | ||
| """Initialization.""" | ||
| tmp = [] | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| qx = [] | ||
| qy = [] | ||
| qz = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 4: | ||
| qx, qy, qz, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| qx, qy, qz, pol = (np.array(i) for i in (qx, qy, qz, pol)) | ||
| norm = np.emath.sqrt(qx * qx + qy * qy + qz * qz) | ||
| norm[np.abs(norm - 1) < 1e-14] = 1 | ||
| qx, qy, qz = (np.true_divide(i, norm) for i in (qx, qy, qz)) | ||
| for i in (qx, qy, qz, pol): | ||
| i.flags.writeable = False | ||
| if i.ndim > 1: | ||
| raise ValueError("invalid shape of parameters") | ||
| self.qx, self.qy, self.qz = qx, qy, qz | ||
| self.pol = np.array(pol.real, int) | ||
| if np.any(self.pol != pol): | ||
| raise ValueError("polarizations must be integer") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| self.lattice = self.kpar = None | ||
| def __getattr__(self, key): | ||
| dct = {"x": "qx", "y": "qy", "z": "qz", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| Alternatively, the string "xyzp", "xyp", or "zp" can be used to access only a | ||
| subset of :attr:`qx`, :attr:`qy`, :attr:`qz`, and :attr:`pol`. | ||
| """ | ||
| res = self.qx[idx], self.qy[idx], self.qz[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or (isinstance(idx, tuple) and idx == ()): | ||
| return res | ||
| return type(self)(zip(*res)) | ||
| @classmethod | ||
| def default(cls, kvecs): | ||
| """Default basis from the given wave vectors. | ||
| For each wave vector the two polarizations 1 and 0 are taken. | ||
| Example: | ||
| >>> PlaneWaveBasisByUnitVector.default([[0, 0, 5], [0, 3, 4]]) | ||
| PlaneWaveBasisByUnitVector( | ||
| qx=[0. 0. 0. 0.], | ||
| qy=[0. 0. 0.6 0.6], | ||
| qz=[1. 1. 0.8 0.8], | ||
| pol=[1 0 1 0], | ||
| ) | ||
| Args: | ||
| kvecs (array-like): Wave vectors in Cartesian coordinates. | ||
| """ | ||
| kvecs = np.atleast_2d(kvecs) | ||
| modes = np.empty((2 * kvecs.shape[0], 4), kvecs.dtype) | ||
| modes[::2, :3] = kvecs | ||
| modes[1::2, :3] = kvecs | ||
| modes[::2, 3] = 1 | ||
| modes[1::2, 3] = 0 | ||
| return cls(modes) | ||
| @classmethod | ||
| def _from_iterable(cls, it): | ||
| if isinstance(cls, PlaneWaveBasisByUnitVector): | ||
| lattice = cls.lattice | ||
| kpar = cls.kpar | ||
| cls = type(cls) | ||
| else: | ||
| lattice = kpar = None | ||
| obj = cls(it) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same | ||
| order. | ||
| """ | ||
| try: | ||
| return self is other or ( | ||
| np.array_equal(self.qx, other.qx) | ||
| and np.array_equal(self.qy, other.qy) | ||
| and np.array_equal(self.qz, other.qz) | ||
| and np.array_equal(self.pol, other.pol) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| def bycomp(self, k0, alignment="xy", material=Material()): | ||
| """Create a :class:`PlaneWaveBasisByComp`. | ||
| The plane wave basis is changed to a partial basis, where only two (real-valued) | ||
| wave vector components are defined the third component is then inferred from the | ||
| dispersion relation, which depends on the wave number and material, and a | ||
| ``modetype`` that specifies the sign of the third component. | ||
| Args: | ||
| alignment (str, optional): Wave vector components that are part of the | ||
| partial basis. Defaults to "xy", other permitted values are "yz" and | ||
| "zx". | ||
| k0 (float, optional): Wave number. If given, it is checked that the current | ||
| basis fulfils the dispersion relation. | ||
| material (:class:`~treams.Material` or tuple): Material definition. Defaults | ||
| to vacuum/air. | ||
| """ | ||
| ks = material.ks(k0)[self.pol] | ||
| if alignment in ("xy", "yz", "zx"): | ||
| kpars = [ks * getattr(self, "q" + s) for s in alignment] | ||
| else: | ||
| raise ValueError(f"invalid alignment '{alignment}'") | ||
| obj = PlaneWaveBasisByComp(zip(*kpars, self.pol), alignment) | ||
| obj.lattice = self.lattice | ||
| obj.kpar = self.kpar | ||
| return obj | ||
| def kvecs(self, k0, material=Material(), modetype=None): | ||
| """Wave vectors. | ||
| Args: | ||
| k0 (float): Wave number. | ||
| material (:class:`~treams.Material` or tuple, optional): Material | ||
| definition. Defaults to vacuum/air. | ||
| modetype (optional): Currently unused for this class. | ||
| """ | ||
| # TODO: check kz depending on modetype (alignment?) | ||
| ks = Material(material).ks(k0)[self.pol] | ||
| return ks * self.qx, ks * self.qy, ks * self.qz | ||
| def permute(self, n=1): | ||
| n = n % 3 | ||
| lattice = None if self.lattice is None else self.lattice.permute(n) | ||
| kpar = None if self.kpar is None else self.kpar.permute(n) | ||
| qx, qy, qz = self.qx, self.qy, self.qz | ||
| for _ in range(n): | ||
| qx, qy, qz = qz, qx, qy | ||
| obj = type(self)(zip(*(qx, qy, qz, self.pol))) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| class PlaneWaveBasisByComp(PlaneWaveBasis): | ||
| """Partial plane wave basis. | ||
| A partial plane wave basis is defined by two wave vector components out of all three | ||
| Cartesian wave vector components ``kx``, ``ky``, and ``kz`` and the polarizations | ||
| ``pol``. Which two components are given is specified in the :attr:`alignment`. This | ||
| basis is mostly used for stratified media that are periodic or uniform in the two | ||
| alignment directions, such that the given wave vector components correspond to the | ||
| diffraction orders. | ||
| For plane waves there exist multiple :ref:`params:Polarizations`, such that | ||
| these modes can refer to either :func:`~treams.special.vpw_A` or | ||
| :func:`~treams.special.vpw_M` and :func:`~treams.special.vpw_N`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``k1``, ``k2``, and | ||
| ``pol``. | ||
| alignment (str, optional): Definition which wave vector components are given. | ||
| Defaults to "xy", other possible values are "yz" and "zx". | ||
| Attributes: | ||
| alignment (str): Alignment of the partial basis. | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| def __init__(self, modes, alignment="xy"): | ||
| """Initialization.""" | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| tmp = [] | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| kx = [] | ||
| ky = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 3: | ||
| kx, ky, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| self._kx, self._ky = [np.real(i) for i in (kx, ky)] | ||
| self.pol = np.array(np.real(pol), int) | ||
| for i, j in [(self._kx, kx), (self._ky, ky), (self.pol, pol)]: | ||
| i.flags.writeable = False | ||
| if i.ndim > 1: | ||
| raise ValueError("invalid shape of parameters") | ||
| if np.any(i != j): | ||
| raise ValueError("invalid value for parameter, must be real") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| if alignment in ("xy", "yz", "zx"): | ||
| self._names = (*("k" + i for i in alignment),) + ("pol",) | ||
| else: | ||
| raise ValueError(f"invalid alignment '{alignment}'") | ||
| self.alignment = alignment | ||
| self.lattice = self.kpar = None | ||
| def permute(self, n=1): | ||
| n = n % 3 | ||
| lattice = None if self.lattice is None else self.lattice.permute(n) | ||
| kpar = None if self.kpar is None else self.kpar.permute(n) | ||
| alignments = {"xy": "yz", "yz": "zx", "zx": "xy"} | ||
| alignment = self.alignment | ||
| for _ in range(n): | ||
| alignment = alignments[alignment] | ||
| obj = self._from_iterable(self, alignment=alignment) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| @property | ||
| def kx(self): | ||
| """X-components of the wave vector. | ||
| If the components are not specified `None` is returned. | ||
| """ | ||
| if self.alignment == "xy": | ||
| return self._kx | ||
| if self.alignment == "zx": | ||
| return self._ky | ||
| return None | ||
| @property | ||
| def ky(self): | ||
| """Y-components of the wave vector. | ||
| If the components are not specified `None` is returned. | ||
| """ | ||
| if self.alignment == "xy": | ||
| return self._ky | ||
| if self.alignment == "yz": | ||
| return self._kx | ||
| return None | ||
| @property | ||
| def kz(self): | ||
| """Z-components of the wave vector. | ||
| If the components are not specified `None` is returned. | ||
| """ | ||
| if self.alignment == "yz": | ||
| return self._ky | ||
| if self.alignment == "zx": | ||
| return self._kx | ||
| return None | ||
| def __getattr__(self, key): | ||
| dct = {"x": "kx", "y": "ky", "z": "kz", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| """ | ||
| res = self._kx[idx], self._ky[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or (isinstance(idx, tuple) and idx == ()): | ||
| return res | ||
| return type(self)(zip(*res)) | ||
| @classmethod | ||
| def default(cls, kpars, alignment="xy"): | ||
| """Default basis from the given wave vectors. | ||
| For each wave vector the two polarizations 1 and 0 are taken. | ||
| Example: | ||
| >>> PlaneWaveBasisByComp.default([[0, 0], [0, 3]]) | ||
| PlaneWaveBasisByComp( | ||
| kx=[0. 0. 0. 0.], | ||
| ky=[0. 0. 3. 3.], | ||
| pol=[1 0 1 0], | ||
| ) | ||
| Args: | ||
| kpars (array-like): Wave vector components in Cartesian coordinates. | ||
| alignment (str, optional): Definition which wave vector components are | ||
| given. Defaults to "xy", other possible values are "yz" and "zx". | ||
| """ | ||
| kpars = np.atleast_2d(kpars) | ||
| modes = np.empty((2 * kpars.shape[0], 3), float) | ||
| modes[::2, :2] = kpars | ||
| modes[1::2, :2] = kpars | ||
| modes[::2, 2] = 1 | ||
| modes[1::2, 2] = 0 | ||
| return cls(modes, alignment=alignment) | ||
| @classmethod | ||
| def diffr_orders(cls, kpar, lattice, bmax): | ||
| """Create a basis set for a two-dimensional periodic system. | ||
| The reciprocal lattice to the given lattice is taken to consider all diffraction | ||
| orders that lie within the defined maximal radius (in reciprocal space). | ||
| Example: | ||
| >>> PlaneWaveBasisByComp.diffr_orders([0, 0], Lattice.square(2 * np.pi), 1) | ||
| PlaneWaveBasisByComp( | ||
| kx=[ 0. 0. 0. 0. 0. 0. 1. 1. -1. -1.], | ||
| ky=[ 0. 0. 1. 1. -1. -1. 0. 0. 0. 0.], | ||
| pol=[1 0 1 0 1 0 1 0 1 0], | ||
| ) | ||
| Args: | ||
| kpar (float): Tangential wave vector components. Ideally they are in the | ||
| first Brillouin zone (use :func:`misc.firstbrillouin2d`). | ||
| lattice (:class:`treams.Lattice` or float): Lattice definition or pitch. | ||
| bmax (float): Maximal change of tangential wave vector components. So, | ||
| this defines a maximal momentum transfer. | ||
| """ | ||
| lattice = Lattice(lattice) | ||
| if lattice.dim != 2: | ||
| raise ValueError("invalid lattice dimensions") | ||
| latrec = lattice.reciprocal | ||
| kpars = kpar + la.diffr_orders_circle(latrec, bmax) @ latrec | ||
| obj = cls.default(kpars, alignment=lattice.alignment) | ||
| obj.lattice = lattice | ||
| obj.kpar = WaveVector(kpar, alignment=lattice.alignment) | ||
| return obj | ||
| @classmethod | ||
| def _from_iterable(cls, it, alignment="xy"): | ||
| if isinstance(cls, PlaneWaveBasisByComp): | ||
| alignment = cls.alignment if alignment is None else alignment | ||
| lattice = cls.lattice | ||
| kpar = cls.kpar | ||
| cls = type(cls) | ||
| else: | ||
| lattice = kpar = None | ||
| obj = cls(it, alignment) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same | ||
| order. | ||
| """ | ||
| try: | ||
| skx, sky, skz = self.kx, self.ky, self.kz | ||
| okx, oky, okz = other.kx, other.ky, other.kz | ||
| return self is other or ( | ||
| (np.array_equal(skx, okx) or (skx is None and okx is None)) | ||
| and (np.array_equal(sky, oky) or (sky is None and oky is None)) | ||
| and (np.array_equal(skz, okz) or (skz is None and okz is None)) | ||
| and np.array_equal(self.pol, other.pol) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| def byunitvector(self, k0, material=Material(), modetype="up"): | ||
| """Create a complete basis :class:`PlaneWaveBasis`. | ||
| A plane wave basis is considered complete, when all three Cartesian components | ||
| and the polarization is defined for each mode. So, the specified wave number, | ||
| material, and modetype is taken to calculate the third Cartesian wave vector. | ||
| The modetype "up" ("down") is for waves propagating in the positive (negative) | ||
| direction with respect to the Cartesian axis that is orthogonal to the | ||
| alignment. | ||
| Args: | ||
| k0 (float): Wave number. | ||
| material (:class:`~treams.Material` or tuple, optional): Material | ||
| definition. Defaults to vacuum/air. | ||
| modetype (str, optional): Propagation direction. Defaults to "up". | ||
| """ | ||
| if modetype not in ("up", "down"): | ||
| raise ValueError("modetype not recognized") | ||
| material = Material(material) | ||
| kx = self._kx | ||
| ky = self._ky | ||
| kz = material.kzs(k0, kx, ky, self.pol) * (2 * (modetype == "up") - 1) | ||
| if self.alignment == "yz": | ||
| kx, ky, kz = kz, kx, ky | ||
| elif self.alignment == "zy": | ||
| kx, ky, kz = ky, kz, kx | ||
| obj = PlaneWaveBasisByUnitVector(zip(kx, ky, kz, self.pol)) | ||
| obj.lattice = self.lattice | ||
| obj.kpar = self.kpar | ||
| return obj | ||
| def kvecs(self, k0, material=Material(), modetype="up"): | ||
| """Wave vectors. | ||
| Args: | ||
| k0 (float): Wave number. | ||
| material (:class:`~treams.Material` or tuple, optional): Material | ||
| definition. Defaults to vacuum/air. | ||
| modetype (str, optional): Propagation direction. Defaults to "up". | ||
| """ | ||
| if modetype not in ("up", "down"): | ||
| raise ValueError("modetype not recognized") | ||
| material = Material(material) | ||
| kx = self._kx | ||
| ky = self._ky | ||
| kz = material.kzs(k0, kx, ky, self.pol) * (2 * (modetype == "up") - 1) | ||
| if self.alignment == "yz": | ||
| return kz, kx, ky | ||
| if self.alignment == "zx": | ||
| return ky, kz, kx | ||
| return kx, ky, kz | ||
| def _raise_basis_error(*args): | ||
| raise TypeError("'basis' must be BasisSet") | ||
| class PhysicsDict(util.AnnotationDict): | ||
| """Physics dictionary. | ||
| Derives from :class:`treams.util.AnnotationDict`. This dictionary has additionally | ||
| several properties defined. | ||
| Attributes: | ||
| basis (:class:`BasisSet`): Basis set. | ||
| k0 (float): Wave number. | ||
| kpar (list): Parallel wave vector components. Usually, this is a list of length | ||
| 3 with its items corresponding to the Cartesian axes. Unspecified items are | ||
| set to `nan`. | ||
| lattice (:class:`~treams.Lattice`): Lattice definition. | ||
| material (:class:`~treams.Material`): Material definition. | ||
| modetype (str): Mode type, for spherical and cylindrical waves this can be | ||
| "incident" and "scattered", for partial plane waves it can be "up" or | ||
| "down". | ||
| poltype (str): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| properties = { | ||
| "basis": ( | ||
| ":class:`~treams.BasisSet`.", | ||
| lambda x: isinstance(x, BasisSet), | ||
| _raise_basis_error, | ||
| ), | ||
| "k0": ("Wave number.", lambda x: isinstance(x, float), float), | ||
| "kpar": ( | ||
| "Wave vector components tangential to the lattice.", | ||
| lambda x: isinstance(x, WaveVector), | ||
| WaveVector, | ||
| ), | ||
| "lattice": ( | ||
| ":class:`~treams.Lattice`.", | ||
| lambda x: isinstance(x, Lattice), | ||
| Lattice, | ||
| ), | ||
| "material": ( | ||
| ":class:`~treams.Material`.", | ||
| lambda x: isinstance(x, Material), | ||
| Material, | ||
| ), | ||
| "modetype": ("Mode type.", lambda x: isinstance(x, str), str), | ||
| "poltype": (":ref:`params:Polarizations`.", lambda x: isinstance(x, str), str,), | ||
| } | ||
| """Special properties tracked by the PhysicsDict.""" | ||
| def __setitem__(self, key, val): | ||
| """Set item specified by key to the defined value. | ||
| When overwriting an existing key an :class:`AnnotationWarning` is emitted. | ||
| Avoid the warning by explicitly deleting the key first. The special attributes | ||
| are cast to their corresponding types. | ||
| Args: | ||
| key (hashable): Key | ||
| val : Value | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| if key not in self.properties: | ||
| raise AttributeError(f"invalid key '{key}'") | ||
| _, testfunc, castfunc = self.properties[key] | ||
| if not testfunc(val): | ||
| val = castfunc(val) | ||
| super().__setitem__(key, val) | ||
| class PhysicsArray(util.AnnotatedArray): | ||
| """Physics-aware array. | ||
| A physics aware array is a special type of :class`~treams.util.AnnotatedArray`. | ||
| Additionally to keeping track of the annotations, it is enhanced by the ability to | ||
| create suiting linear operators to perform tasks like rotations, translations, or | ||
| expansions into different basis sets and by applying special rules for the | ||
| physical properties of :class:`PhysicsDict` upon matrix multiplications, see also | ||
| :meth:`__matmul__`. | ||
| """ | ||
| _scales = {"basis"} | ||
| changepoltype = op.OperatorAttribute(op.ChangePoltype) | ||
| """Polarization change matrix, see also :class:`treams.operators.ChangePoltype`.""" | ||
| efield = op.OperatorAttribute(op.EField) | ||
| hfield = op.OperatorAttribute(op.HField) | ||
| dfield = op.OperatorAttribute(op.DField) | ||
| bfield = op.OperatorAttribute(op.BField) | ||
| gfield = op.OperatorAttribute(op.GField) | ||
| ffield = op.OperatorAttribute(op.FField) | ||
| """Field evaluation matrix, see also :class:`treams.operators.EField`.""" | ||
| expand = op.OperatorAttribute(op.Expand) | ||
| """Expansion matrix, see also :class:`treams.operators.Expand`.""" | ||
| expandlattice = op.OperatorAttribute(op.ExpandLattice) | ||
| """Lattice expansion matrix, see also :class:`treams.operators.ExpandLattice`.""" | ||
| permute = op.OperatorAttribute(op.Permute) | ||
| """Permutation matrix, see also :class:`treams.operators.Permute`.""" | ||
| rotate = op.OperatorAttribute(op.Rotate) | ||
| """Rotation matrix, see also :class:`treams.operators.Rotate`.""" | ||
| translate = op.OperatorAttribute(op.Translate) | ||
| """Translation matrix, see also :class:`treams.operators.Translate`.""" | ||
| def __init__(self, arr, ann=(), /, **kwargs): | ||
| """Initialization.""" | ||
| super().__init__(arr, ann, **kwargs) | ||
| self._check() | ||
| @property | ||
| def ann(self): | ||
| """Array annotations.""" | ||
| # necessary to define the setter below | ||
| return super().ann | ||
| def __getattr__(self, key): | ||
| try: | ||
| return super().__getattr__(key) | ||
| except AttributeError as err: | ||
| if key in PhysicsDict.properties: | ||
| return None | ||
| raise err from None | ||
| def __setattr__(self, key, val): | ||
| if key in PhysicsDict.properties: | ||
| val = (val,) * self.ndim if not isinstance(val, tuple) else val | ||
| self.ann.as_dict[key] = val | ||
| else: | ||
| super().__setattr__(key, val) | ||
| @ann.setter | ||
| def ann(self, ann): | ||
| """Set array annotations. | ||
| See also :meth:`treams.util.AnnotatedArray.__setitem__`. | ||
| """ | ||
| self._ann = util.AnnotationSequence(*(({},) * self.ndim), mapping=PhysicsDict) | ||
| self._ann.update(ann) | ||
| def __repr__(self): | ||
| """String representation. | ||
| For a more managable output only the special physics properties are shown | ||
| alongside the array itself. | ||
| """ | ||
| repr_arr = " " + repr(self._array)[6:-1].replace("\n ", "\n") + "," | ||
| for key in PhysicsDict.properties: | ||
| if getattr(self, key) is not None: | ||
| repr_arr += f"\n {key}={repr(getattr(self, key))}," | ||
| return f"{self.__class__.__name__}(\n{repr_arr}\n)" | ||
| def _check(self): | ||
| """Run checks to validate the physical properties. | ||
| The checks are run on the last two dimensions. They include: | ||
| * Dispersion relation checks for :class:`PlaneWaveBasis` if `basis`, `k0` | ||
| and `material` is defined` | ||
| * `parity` polarizations are not physically permitted in chiral media. | ||
| * All lattices explicitly given and in the basis hints must be compatible. | ||
| * All tangential wave vector compontents must be compatible. | ||
| """ | ||
| for a in self.ann[-2:]: | ||
| k0 = a.get("k0") | ||
| material = a.get("material") | ||
| modetype = a.get("modetype") | ||
| poltype = a.get("poltype") | ||
| basis = a.get("basis", namedtuple("_basis", "lattice kpar")(None, None)) | ||
| if poltype == "parity" and getattr(material, "ischiral", False): | ||
| raise ValueError("poltype 'parity' not permitted for chiral material") | ||
| def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): | ||
| """Implement ufunc API. | ||
| Additionally to keeping track of the annotations the special properties of a | ||
| PhysicsArray are also "transparent" in matrix multiplications. | ||
| """ | ||
| res = super().__array_ufunc__(ufunc, method, *inputs, **kwargs) | ||
| if ( | ||
| ufunc is np.matmul | ||
| and method == "__call__" | ||
| and not isinstance(res, np.generic) | ||
| ): | ||
| axes = kwargs.get( | ||
| "axes", [tuple(range(-min(np.ndim(i), 2), 0)) for i in inputs + (res,)] | ||
| ) | ||
| anns = [ | ||
| i.ann[ax] if hasattr(i, "ann") else [{}, {}] | ||
| for i, ax in zip(inputs, axes) | ||
| ] | ||
| for name in PhysicsDict.properties: | ||
| if name in anns[0][-1] and all(name not in a for a in anns[1]): | ||
| res.ann[axes[-1][-1]].setdefault(name, anns[0][-1][name]) | ||
| if name in anns[1][0] and all(name not in a for a in anns[0]): | ||
| res.ann[axes[-1][0]].setdefault(name, anns[1][0][name]) | ||
| return res |
| import collections | ||
| import numpy as np | ||
| import treams.lattice as la | ||
| class Lattice: | ||
| """Lattice definition. | ||
| The lattice can be one-, two-, or three-dimensional. If it is not three-dimensional | ||
| it is required to be embedded into a lower dimensional subspace that is aligned with | ||
| the Cartesian axes. The default alignment for one-dimensional lattices is along the | ||
| z-axis and for two-dimensional lattices it is in the x-y-plane. | ||
| For a one-dimensional lattice the definition consists of simply the value of the | ||
| lattice pitch. Higher-dimensional lattices are defined by (2, 2)- or (3, 3)-arrays. | ||
| If the arrays are diagonal it is sufficient to specify the (2,)- or (3,)-array | ||
| diagonals. Alternatively, if another instance of a Lattice is given, the defined | ||
| alignment is extracted, which can be used to separate a lower-dimensional | ||
| sublattice. | ||
| Lattices are immutable objects. | ||
| Example: | ||
| >>> Lattice([1, 2]) | ||
| Lattice([[1. 0.] | ||
| [0. 2.]], alignment='xy') | ||
| >>> Lattice(_, 'x') | ||
| Lattice(1.0, alignment='x') | ||
| Args: | ||
| arr (float, array, Lattice): Lattice definition. Each row corresponds to one | ||
| lattice vector, each column to the axis defined in `alignment`. | ||
| alignment (str, optional): Alignment of the lattice. Possible values are 'x', | ||
| 'y', 'z', 'xy', 'yz', 'zx', and 'xyz'. | ||
| """ | ||
| _allowed_alignments = { | ||
| "x": "y", | ||
| "y": "z", | ||
| "z": "x", | ||
| "xy": "yz", | ||
| "yz": "zx", | ||
| "zx": "xy", | ||
| "xyz": "xyz", | ||
| } | ||
| def __init__(self, arr, alignment=None): | ||
| """Initialization.""" | ||
| if isinstance(arr, Lattice): | ||
| self._alignment = arr.alignment if alignment is None else alignment | ||
| self._lattice = arr._sublattice(self._alignment)[...] | ||
| self._reciprocal = ( | ||
| 2 * np.pi / self[...] if self.dim == 1 else la.reciprocal(self[...]) | ||
| ) | ||
| return | ||
| if arr is None: | ||
| raise ValueError("Lattice cannot be 'None'") | ||
| arr = np.array(arr, float) | ||
| arr.flags.writeable = False | ||
| alignments = {1: ("z", "x", "y"), 4: ("xy", "yz", "zx"), 9: ("xyz",)} | ||
| if arr.ndim < 3 and arr.size == 1: | ||
| self._lattice = np.squeeze(arr) | ||
| elif arr.ndim == 1 and arr.size in (2, 3): | ||
| self._lattice = np.diag(arr) | ||
| elif arr.ndim == 2 and arr.shape[0] == arr.shape[1] and arr.shape[0] in (2, 3): | ||
| self._lattice = arr | ||
| else: | ||
| raise ValueError(f"invalid shape '{arr.shape}'") | ||
| alignment = ( | ||
| alignments[self._lattice.size][0] if alignment is None else alignment | ||
| ) | ||
| if alignment not in alignments[self._lattice.size]: | ||
| raise ValueError(f"invalid alignment '{alignment}'") | ||
| self._alignment = alignment | ||
| if self.volume == 0: | ||
| raise ValueError("linearly dependent lattice vectors") | ||
| self._reciprocal = ( | ||
| 2 * np.pi / self[...] if self.dim == 1 else la.reciprocal(self[...]) | ||
| ) | ||
| @property | ||
| def alignment(self): | ||
| """The alignment of the lattice in three-dimensional space. | ||
| For three-dimensional lattices it is always 'xyz' but lower-dimensional lattices | ||
| have to be aligned with a subset of the axes. | ||
| Returns: | ||
| str | ||
| """ | ||
| return self._alignment | ||
| @classmethod | ||
| def square(cls, pitch, alignment=None): | ||
| """Create a two-dimensional square lattice. | ||
| Args: | ||
| pitch (float): Lattice constant. | ||
| alignment (str, optional): Alignment of the two-dimensional lattice in the | ||
| three-dimensional space. Defaults to 'xy'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls(2 * [pitch], alignment) | ||
| @classmethod | ||
| def cubic(cls, pitch, alignment=None): | ||
| """Create a three-dimensional cubic lattice. | ||
| Args: | ||
| pitch (float): Lattice constant. | ||
| alignment (str, optional): Alignment of the lattice. Defaults to 'xyz'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls(3 * [pitch], alignment) | ||
| @classmethod | ||
| def rectangular(cls, x, y, alignment=None): | ||
| """Create a two-dimensional rectangular lattice. | ||
| Args: | ||
| x (float): Lattice constant along the first dimension. For the default | ||
| alignment this corresponds to the x-axis. | ||
| y (float): Lattice constant along the second dimension. For the default | ||
| alignment this corresponds to the y-axis. | ||
| alignment (str, optional): Alignment of the two-dimensional lattice in the | ||
| three-dimensional space. Defaults to 'xy'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls([x, y], alignment) | ||
| @classmethod | ||
| def orthorhombic(cls, x, y, z, alignment=None): | ||
| """Create a three-dimensional orthorhombic lattice. | ||
| Args: | ||
| x (float): Lattice constant along the first dimension. For the default | ||
| alignment this corresponds to the x-axis. | ||
| y (float): Lattice constant along the second dimension. For the default | ||
| alignment this corresponds to the y-axis. | ||
| z (float): Lattice constant along the third dimension. For the default | ||
| alignment this corresponds to the z-axis. | ||
| alignment (str, optional): Alignment of the lattice. Defaults to 'xyz'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls([x, y, z], alignment) | ||
| @classmethod | ||
| def hexagonal(cls, pitch, height=None, alignment=None): | ||
| """Create a hexagonal lattice. | ||
| The lattice is two-dimensional if no height is specified else it is | ||
| three-dimensional | ||
| Args: | ||
| pitch (float): Lattice constant. | ||
| height (float, optional): Separation along the third axis for a | ||
| three-dimensional lattice. | ||
| alignment (str, optional): Alignment of the two-dimensional lattice in the | ||
| three-dimensional space. Defaults to either 'xy' or 'xyz' in the two- | ||
| or three-dimensional case, respectively. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if height is None: | ||
| return cls( | ||
| np.array([[pitch, 0], [0.5 * pitch, np.sqrt(0.75) * pitch]]), alignment | ||
| ) | ||
| return cls( | ||
| np.array( | ||
| [[pitch, 0, 0], [0.5 * pitch, np.sqrt(0.75) * pitch, 0], [0, 0, height]] | ||
| ), | ||
| alignment, | ||
| ) | ||
| def __eq__(self, other): | ||
| """Equality. | ||
| Two lattices are considered equal when they have the same dimension, alignment, | ||
| and lattice vectors. | ||
| Args: | ||
| other (Lattice): Lattice to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return other is not None and ( | ||
| self is other | ||
| or ( | ||
| self.alignment == other.alignment | ||
| and self.dim == other.dim | ||
| and np.all(self[...] == other[...]) | ||
| ) | ||
| ) | ||
| @property | ||
| def dim(self): | ||
| """Dimension of the lattice. | ||
| Returns: | ||
| int | ||
| """ | ||
| if self._lattice.ndim == 0: | ||
| return 1 | ||
| return self._lattice.shape[0] | ||
| @property | ||
| def volume(self): | ||
| """(Generalized) volume of the lattice. | ||
| The value gives the lattice pitch, area, or volume depending on its dimension. | ||
| The volume is signed. | ||
| Returns: | ||
| float | ||
| """ | ||
| if self.dim == 1: | ||
| return self._lattice | ||
| return la.volume(self._lattice) | ||
| @property | ||
| def reciprocal(self): | ||
| r"""Reciprocal lattice. | ||
| The reciprocal lattice to a given lattice with dimension :math:`d` and lattice | ||
| vectors :math:`\boldsymbol a_i` for :math:`i \in \{1, \dots, d\}` is defined by | ||
| lattice vectors :math:`\boldsymbol b_j` with :math:`j \in \{1, \dots, d\}` such | ||
| that :math:`\boldsymbol a_i \boldsymbol b_j = 2 \pi \delta_{ij}` is fulfilled. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return self._reciprocal | ||
| def __getitem__(self, idx): | ||
| """Index into the lattice. | ||
| Indexing can be used to obtain entries from the lattice vector definitions or | ||
| by using the Ellipsis or empty tuple to obtain the full array. | ||
| Returns: | ||
| float | ||
| """ | ||
| return self._lattice[idx] | ||
| def __str__(self): | ||
| """String representation. | ||
| Simply returns the lattice pitch or lattice vectors. | ||
| Returns: | ||
| str | ||
| """ | ||
| return str(self._lattice) | ||
| def __repr__(self): | ||
| """Representation. | ||
| The result can be used to recreate an equal instance. | ||
| Returns: | ||
| str | ||
| """ | ||
| string = str(self._lattice).replace("\n", "\n ") | ||
| return f"Lattice({string}, alignment='{self.alignment}')" | ||
| def _sublattice(self, key): | ||
| """Get the sublattice defined by key. | ||
| This function is called when one gives another instance of Lattice to the | ||
| constructor. | ||
| Args: | ||
| key (str): Alignment of the sublattice to extract. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| key = key.lower() | ||
| if self.dim == 1: | ||
| if key == self.alignment: | ||
| return Lattice(self[...], key) | ||
| raise ValueError(f"sublattice with key '{key}' not availale") | ||
| idx = [] | ||
| for c in key: | ||
| idx.append(self.alignment.find(c)) | ||
| if -1 in idx or key not in self._allowed_alignments: | ||
| raise ValueError(f"sublattice with key '{key}' not availale") | ||
| idx_opp = [i for i in range(self.dim) if i not in idx] | ||
| mask = np.any(self[:, idx] != 0, axis=-1) | ||
| if ( | ||
| (self[mask, :][:, idx_opp] != 0).any() | ||
| or (self[np.logical_not(mask), :][:, idx] != 0).any() | ||
| or len(idx) != sum(mask) | ||
| ): | ||
| raise ValueError("cannot determine sublattice") | ||
| return Lattice(self[mask, :][:, idx], key) | ||
| def permute(self, n=1): | ||
| """Permute the lattice orientation. | ||
| Get a new lattice with the alignment permuted. | ||
| Examples: | ||
| >>> Lattice.hexagonal(1, 2).permute() | ||
| Lattice([[0. 1. 0. ] | ||
| [0. 0.5 0.866] | ||
| [2. 0. 0. ]], alignment='xyz') | ||
| >>> Lattice.hexagonal(1).permute() | ||
| Lattice([[1. 0. ] | ||
| [0.5 0.866]], alignment='yz') | ||
| Args: | ||
| n (int, optional): Number of repeated permutations. Defaults to `1`. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if n != int(n): | ||
| raise ValueError("'n' must be integer") | ||
| n = n % 3 | ||
| lattice = self[...] | ||
| alignment = self.alignment | ||
| if self.dim == 3: | ||
| while n > 0: | ||
| lattice = lattice[:, [2, 0, 1]] | ||
| n -= 1 | ||
| else: | ||
| while n > 0: | ||
| alignment = self._allowed_alignments[alignment] | ||
| n -= 1 | ||
| return Lattice(lattice, alignment) | ||
| def __bool__(self): | ||
| """Lattice instances always equate to True. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return True | ||
| def __or__(self, other): | ||
| """Merge two lattices. | ||
| Two lattices are combined into one if possible. | ||
| Example: | ||
| >>> Lattice(1, 'x') | Lattice(2) | ||
| Lattice([[2. 0.] | ||
| [0. 1.]], alignment='zx') | ||
| Args: | ||
| other (Lattice): Lattice to merge. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if other is None or self == other: | ||
| return Lattice(self) | ||
| alignment = list({c for lat in (self, other) for c in lat.alignment}) | ||
| alignment = "".join(sorted(alignment)) | ||
| if alignment == "xz": | ||
| alignment = "zx" | ||
| ndim = len(alignment) | ||
| if ndim == 2: | ||
| if self.dim == 1 == other.dim: | ||
| if alignment.find(self.alignment) == 0: | ||
| return Lattice([self[...], other[...]], alignment) | ||
| return Lattice([other[...], self[...]], alignment) | ||
| if self.dim == 2 and Lattice(self, other.alignment) == other: | ||
| return Lattice(self) | ||
| if other.dim == 2 and Lattice(other, self.alignment) == self: | ||
| return Lattice(other) | ||
| elif ndim == 3: | ||
| if self.dim == 3 and Lattice(self, other.alignment) == other: | ||
| return Lattice(self) | ||
| if other.dim == 3 and Lattice(other, self.alignment) == self: | ||
| return Lattice(other) | ||
| if self.dim == 2 == other.dim: | ||
| arr = np.zeros(3) | ||
| for i, c in enumerate("xyz"): | ||
| if c in self.alignment: | ||
| la0 = Lattice(self, c) | ||
| else: | ||
| la0 = None | ||
| if c in other.alignment: | ||
| la1 = Lattice(other, c) | ||
| else: | ||
| la1 = None | ||
| if None not in (la0, la1) and la0 != la1: | ||
| raise ValueError("cannot combine lattices") | ||
| arr[i] = la0[...] if la0 is not None else la1[...] | ||
| return Lattice(arr) | ||
| arr = np.zeros((3, 3)) | ||
| la0, la1 = (self, other) if self.dim == 1 else (other, self) | ||
| if la0.alignment == "x": | ||
| arr[0, 0] = la0[...] | ||
| arr[1:, 1:] = la1[...] | ||
| return Lattice(arr) | ||
| if la0.alignment == "y": | ||
| arr[1, 1] = la0[...] | ||
| arr[[[2], [0]], [2, 0]] = la1[...] | ||
| return Lattice(arr) | ||
| if la0.alignment == "z": | ||
| arr[2, 2] = la0[...] | ||
| arr[:2, :2] = la1[...] | ||
| return Lattice(arr) | ||
| raise ValueError("cannot combine lattices") | ||
| def __and__(self, other): | ||
| """Intersect two lattices. | ||
| The intersection of two lattices is taken if possible. | ||
| Example: | ||
| >>> Lattice([1, 2]) & Lattice([2, 3], 'yz') | ||
| Lattice(2.0, alignment='y') | ||
| Args: | ||
| other (Lattice): Lattice to intersect. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if other is None: | ||
| return None | ||
| if self == other: | ||
| return Lattice(self) | ||
| alignment = list({c for c in self.alignment if c in other.alignment}) | ||
| if len(alignment) == 0: | ||
| raise ValueError("cannot combine lattices") | ||
| alignment = "".join(sorted(alignment)) | ||
| if alignment == "xz": | ||
| alignment = "zx" | ||
| a, b = Lattice(self, alignment), Lattice(other, alignment) | ||
| if a == b: | ||
| return a | ||
| raise ValueError("cannot combine lattices") | ||
| def __le__(self, other): | ||
| """Test if one lattice includes another. | ||
| Example: | ||
| >>> Lattice(3) <= Lattice([1, 2, 3]) | ||
| True | ||
| Args: | ||
| other (Lattice): Lattice to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| try: | ||
| lat = Lattice(other, self.alignment) | ||
| except ValueError: | ||
| return False | ||
| return lat == self | ||
| def isdisjoint(self, other): | ||
| """Test if lattices are disjoint. | ||
| Lattices are considered disjoint if their alignments are disjoint. | ||
| Example: | ||
| >>> Lattice([1, 2, 3]).isdisjoint(Lattice(1)) | ||
| False | ||
| Args: | ||
| other (Lattice): Lattice to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| for c in other.alignment: | ||
| if c in self.alignment: | ||
| return False | ||
| return True | ||
| class WaveVector(collections.abc.Sequence): | ||
| def __init__(self, seq=(), alignment=None): | ||
| try: | ||
| length = len(seq) | ||
| except TypeError: | ||
| length = 1 | ||
| seq = [seq] | ||
| if length == 3: | ||
| self._vec = tuple(seq) | ||
| elif length == 2: | ||
| if alignment in ("xy", None): | ||
| self._vec = (seq[0], seq[1], np.nan) | ||
| elif alignment == "yz": | ||
| self._vec = (np.nan, seq[0], seq[1]) | ||
| elif alignment == "zx": | ||
| self._vec = (seq[1], np.nan, seq[0]) | ||
| else: | ||
| raise ValueError(f"invalid alignment: {alignment}") | ||
| elif length == 1: | ||
| if alignment in ("z", None): | ||
| self._vec = (np.nan, np.nan, seq[0]) | ||
| elif alignment == "y": | ||
| self._vec = (np.nan, seq[0], np.nan) | ||
| elif alignment == "x": | ||
| self._vec = (seq[0], np.nan, np.nan) | ||
| else: | ||
| raise ValueError(f"invalid alignment: {alignment}") | ||
| elif length == 0: | ||
| self._vec = (np.nan,) * 3 | ||
| else: | ||
| raise ValueError("invalid sequence") | ||
| def __str__(self): | ||
| return str(self._vec) | ||
| def __repr__(self): | ||
| return self.__class__.__name__ + str(self._vec) | ||
| def __eq__(self, other): | ||
| other = WaveVector(other) | ||
| for a, b in zip(self, other): | ||
| if a != b and not (np.isnan(a) and np.isnan(b)): | ||
| return False | ||
| return True | ||
| def __len__(self): | ||
| return 3 | ||
| def __getitem__(self, key): | ||
| return self._vec[key] | ||
| def __or__(self, other): | ||
| other = WaveVector(other) | ||
| seq = () | ||
| for a, b in zip(self, other): | ||
| isnan = np.isnan(a) or np.isnan(b) | ||
| if a != b and not isnan: | ||
| raise ValueError("non-matching WaveVector") | ||
| seq += (np.nan,) if isnan else (a,) | ||
| return WaveVector(seq) | ||
| def __and__(self, other): | ||
| other = WaveVector(other) | ||
| seq = () | ||
| for a, b in zip(self, other): | ||
| if a != b and not (np.isnan(a) or np.isnan(b)): | ||
| raise ValueError("non-matching WaveVector") | ||
| seq += (b,) if np.isnan(a) else (a,) | ||
| return WaveVector(seq) | ||
| def permute(self, n=1): | ||
| x, y, z = self | ||
| n = n % 3 | ||
| for _ in range(n): | ||
| x, y, z = z, x, y | ||
| return WaveVector((x, y, z)) | ||
| def __le__(self, other): | ||
| other = WaveVector(other) | ||
| for a, b in zip(self, other): | ||
| if a != b and not np.isnan(b): | ||
| return False | ||
| return True | ||
| def isdisjoint(self, other): | ||
| other = WaveVector(other) | ||
| for a, b in zip(self, other): | ||
| if not (np.isnan(a) or np.isnan(b)): | ||
| return False | ||
| return True |
| import numpy as np | ||
| from treams import misc | ||
| class Material: | ||
| r"""Material definition. | ||
| The material properties are defined in the frequency domain through scalar values | ||
| for permittivity :math:`\epsilon`, permeability :math:`\mu`, and the chirality | ||
| parameter :math:`\kappa`. Materials are, thus, assumed to be linear, | ||
| time-invariant, homogeneous, isotropic, and local. Also, it is assumed that they | ||
| have no gain. The relation of the electric and magnetic fields is defined by | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| \frac{1}{\epsilon_0} \boldsymbol D \\ | ||
| c \boldsymbol B | ||
| \end{pmatrix} | ||
| = | ||
| \begin{pmatrix} | ||
| \epsilon & \mathrm i \kappa \\ | ||
| -\mathrm i \kappa & \mu | ||
| \end{pmatrix} | ||
| \begin{pmatrix} | ||
| \boldsymbol E \\ | ||
| Z_0 \boldsymbol H | ||
| \end{pmatrix} | ||
| for a fixed vacuum wave number :math:`k_0` and spatial position | ||
| :math:`\boldsymbol r` with :math:`\epsilon_0` the vacuum permittivity, :math:`c` the | ||
| speed of light in vacuum, and :math:`Z_0` the vacuum impedance. | ||
| Args: | ||
| epsilon (optional, complex): Relative permittivity. Defaults to 1. | ||
| mu (optional, complex): Relative permeability. Defaults to 1. | ||
| kappa (optional, complex): Chirality parameter. Defaults to 0. | ||
| """ | ||
| def __init__(self, epsilon=1, mu=1, kappa=0): | ||
| """Initialization.""" | ||
| if isinstance(epsilon, Material): | ||
| epsilon, mu, kappa = epsilon() | ||
| elif isinstance(epsilon, (tuple, list, np.ndarray)): | ||
| if len(epsilon) == 0: | ||
| epsilon = 1 | ||
| elif len(epsilon) == 1: | ||
| epsilon = epsilon[0] | ||
| elif len(epsilon) == 2: | ||
| epsilon, mu = epsilon | ||
| elif len(epsilon) == 3: | ||
| epsilon, mu, kappa = epsilon | ||
| else: | ||
| raise ValueError("invalid material definition") | ||
| self._epsilon = epsilon | ||
| self._mu = mu | ||
| self._kappa = kappa | ||
| @property | ||
| def epsilon(self): | ||
| """Relative permittivity. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| return self._epsilon | ||
| @property | ||
| def mu(self): | ||
| """Relative permeability. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| return self._mu | ||
| @property | ||
| def kappa(self): | ||
| """Chirality parameter. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| return self._kappa | ||
| def __iter__(self): | ||
| """Iterator for a tuple containing the material parameters. | ||
| Useful for unpacking the material parameters into a function that takes these | ||
| parameters separately, e.g. ``foo(*material)``. | ||
| """ | ||
| return iter((self.epsilon, self.mu, self.kappa)) | ||
| @classmethod | ||
| def from_n(cls, n=1, impedance=None, kappa=0): | ||
| r"""Create material from refractive index and relative impedance. | ||
| This function calculates the relative permeability and permittivity with | ||
| :math:`\epsilon = \frac{n}{Z}` and :math:`\mu = nZ`. The chirality parameter is | ||
| considered separately. | ||
| Note: | ||
| The refractive index is defined independently from the chirality parameter | ||
| here. For an alternative definition of the refractive index, see also | ||
| :func:`Material.from_nmp`. | ||
| Args: | ||
| n (complex, optional): Refractive index. Defaults to 1. | ||
| impedance(complex, optional): Relative impedance. Defaults to the inverse | ||
| of the refractive index. | ||
| kappa (complex, optional): Chirality parameter. Defaults to 0. | ||
| Returns: | ||
| Material | ||
| """ | ||
| impedance = 1 / n if impedance is None else impedance | ||
| epsilon = n / impedance | ||
| mu = n * impedance | ||
| return cls(epsilon, mu, kappa) | ||
| @classmethod | ||
| def from_nmp(cls, ns=(1, 1), impedance=None): | ||
| r"""Create material from refractive indices of both helicities. | ||
| This function calculates the relative permeability and permittivity and the | ||
| chirality parameter with :math:`\epsilon = \frac{n_+ + n_-}{2Z}`, | ||
| :math:`\mu = \frac{(n_+ + n_-)Z}{2}` and :math:`\mu = \frac{(n_+ - n_-)}{2}`. | ||
| Note: | ||
| Two refractive indices are defined here that depend on the chirality | ||
| parameter. For an alternative definition of the refractive index, see also | ||
| :func:`Material.from_n`. | ||
| Args: | ||
| ns ((2,)-array-like, optional): Negative and positive helicity refractive | ||
| index. Defaults to (1, 1). | ||
| impedance(complex, optional): Relative impedance. Defaults to the inverse of | ||
| the average refractive index. | ||
| Returns: | ||
| Material | ||
| """ | ||
| n = sum(ns) * 0.5 | ||
| kappa = (ns[1] - ns[0]) * 0.5 | ||
| return cls.from_n(n, impedance, kappa) | ||
| @property | ||
| def n(self): | ||
| r"""Refractive index. | ||
| The refractive index is defined by :math:`n = \sqrt{\epsilon \mu}`, with an | ||
| enforced non-negative imaginary part. | ||
| Note: | ||
| The refractive index returned is independent from the chirality parameter. | ||
| For an alternative definition of the refractive index, see also | ||
| :func:`Material.nmp`. | ||
| Returns: | ||
| complex | ||
| """ | ||
| n = np.sqrt(self.epsilon * self.mu) | ||
| if n.imag < 0: | ||
| n = -n | ||
| return n | ||
| @property | ||
| def nmp(self): | ||
| r"""Refractive indices of both helicities. | ||
| The refractive indices are defined by | ||
| :math:`n_\pm = \sqrt{\epsilon \mu} \pm \kappa`, with an enforced non-negative | ||
| imaginary part. | ||
| Note: | ||
| The refractive indices returned depend on the chirality parameter. For an | ||
| alternative definition of the refractive index, see also :func:`Material.n`. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| return misc.refractive_index(self.epsilon, self.mu, self.kappa) | ||
| @property | ||
| def impedance(self): | ||
| r"""Relative impedance. | ||
| The relative impedance is defined by :math:`Z = \sqrt{\frac{\epsilon}{\mu}}`. | ||
| Returns: | ||
| complex | ||
| """ | ||
| return np.sqrt(self.mu / self.epsilon) | ||
| def __call__(self): | ||
| """Return a tuple containing all material parameters. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| return self.epsilon, self.mu, self.kappa | ||
| def __eq__(self, other): | ||
| """Compare material parameters. | ||
| Materials are considered equal, when all material parameters are equal. Also, | ||
| compares with objects that contain at most three values. | ||
| Returns: | ||
| bool | ||
| """ | ||
| if other is None: | ||
| return False | ||
| if not isinstance(other, Material): | ||
| other = Material(*other) | ||
| return ( | ||
| self.epsilon == other.epsilon | ||
| and self.mu == other.mu | ||
| and self.kappa == other.kappa | ||
| ) | ||
| @property | ||
| def ischiral(self): | ||
| """Test if the material is chiral. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return self.kappa != 0 | ||
| @property | ||
| def isreal(self): | ||
| """Test if the material has purely real parameters. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return all(i.imag == 0 for i in self) | ||
| def __str__(self): | ||
| """All three material parameters. | ||
| Returns: | ||
| str | ||
| """ | ||
| return "(" + ", ".join([str(i) for i in self()]) + ")" | ||
| def __repr__(self): | ||
| """Representation that allows recreating the object. | ||
| Returns: | ||
| str | ||
| """ | ||
| return self.__class__.__name__ + str(self) | ||
| def ks(self, k0): | ||
| """Return the wave numbers in the medium for both polarizations. | ||
| The first value corresponds to negative helicity and the second to positive | ||
| helicity. For achiral materials where parity polarizations can be used both | ||
| values are equal. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| return k0 * self.nmp | ||
| def krhos(self, k0, kz, pol): | ||
| r"""The (cylindrically) radial part of the wave vector. | ||
| The cylindrically radial part is defined by :math:`k_\rho = \sqrt(k^2 - k_z^2)`. | ||
| In case of chiral materials :math:`k` and so :math`k_\rho` depends on the | ||
| polarization. The returned values have non-negative imaginary parts. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| kz (float, array-like): z-component of the wave vector | ||
| pol (int, array-like): Polarization indices | ||
| (:ref:`params:Polarizations`). | ||
| Returns: | ||
| complex, array-like | ||
| """ | ||
| ks = self.ks(k0)[pol] | ||
| return misc.wave_vec_z(kz, 0, ks) | ||
| def kzs(self, k0, kx, ky, pol): | ||
| r"""The z-component of the wave vector. | ||
| The z-component of the wave vector is defined by | ||
| :math:`k_z = \sqrt(k^2 - k_x^2 - k_y^2)`. In case of chiral materials :math:`k` | ||
| and so :math`k_z` depends on the polarization. The returned values have | ||
| non-negative imaginary parts. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| kx (float, array-like): x-component of the wave vector | ||
| ky (float, array-like): y-component of the wave vector | ||
| pol (int, array-like): Polarization indices | ||
| (:ref:`params:Polarizations`). | ||
| Returns: | ||
| complex, array-like | ||
| """ | ||
| ks = self.ks(k0)[pol] | ||
| return misc.wave_vec_z(kx, ky, ks) |
Sorry, the diff of this file is too big to display
| import copy | ||
| import numpy as np | ||
| from treams import config, util | ||
| from treams._core import PhysicsArray | ||
| from treams._core import PlaneWaveBasisByComp as PWBC | ||
| from treams._material import Material | ||
| from treams._operators import translate | ||
| from treams._tmatrix import TMatrixC | ||
| from treams.coeffs import fresnel | ||
| class SMatrix(PhysicsArray): | ||
| """S-matrix for a plane wave.""" | ||
| def _check(self): | ||
| super()._check() | ||
| shape = np.shape(self) | ||
| if len(shape) != 2 or shape[0] != shape[1]: | ||
| raise util.AnnotationError(f"invalid shape: '{shape}'") | ||
| if not isinstance(self.k0, (int, float, np.floating, np.integer)): | ||
| raise util.AnnotationError("invalid k0") | ||
| if self.poltype is None: | ||
| self.poltype = config.POLTYPE | ||
| if self.poltype not in ("parity", "helicity"): | ||
| raise util.AnnotationError("invalid poltype") | ||
| material = (None, None) if self.material is None else self.material | ||
| if isinstance(material, tuple): | ||
| self.material = tuple(Material() if m is None else m for m in material) | ||
| if self.basis is None: | ||
| raise util.AnnotationError("basis not set") | ||
| if not isinstance(self.basis, PWBC): | ||
| self.basis = self.basis.bycomp(self.k0, self.material) | ||
| class OperatorAttributeSMatrices: | ||
| def __init__(self, name): | ||
| self._name = name | ||
| self._obj = self._objtype = None | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| self._objtype = type(obj) if objtype is None else objtype | ||
| return self | ||
| def __call__(self, *args, **kwargs): | ||
| res = [ | ||
| [getattr(ii, self._name)(*args, **kwargs) for ii in i] for i in self._obj | ||
| ] | ||
| return self._objtype(res) | ||
| def apply_left(self, *args, **kwargs): | ||
| res = [ | ||
| [getattr(ii, self._name).apply_left(*args, **kwargs) for ii in i] | ||
| for i in self._obj | ||
| ] | ||
| return self._objtype(res) | ||
| def apply_right(self, *args, **kwargs): | ||
| res = [ | ||
| [getattr(ii, self._name).apply_right(*args, **kwargs) for ii in i] | ||
| for i in self._obj | ||
| ] | ||
| return self._objtype(res) | ||
| class SMatrices: | ||
| r"""Collection of four S-matrices with a plane wave basis. | ||
| The S-matrix describes the scattering of incoming into outgoing modes using a plane | ||
| wave basis, with functions :func:`treams.special.vsw_A`, | ||
| :func:`treams.special.vsw_M`, and :func:`treams.special.vsw_N`. The primary | ||
| direction of propagation is parallel or anti-parallel to the z-axis. The scattering | ||
| object itself is infinitely extended in the x- and y-directions. The S-matrix is | ||
| divided into four submatrices :math:`S_{\uparrow \uparrow}`, | ||
| :math:`S_{\uparrow \downarrow}`, :math:`S_{\downarrow \uparrow}`, and | ||
| :math:`S_{\downarrow \downarrow}`: | ||
| .. math:: | ||
| S = \begin{pmatrix} | ||
| S_{\uparrow \uparrow} & S_{\uparrow \downarrow} \\ | ||
| S_{\downarrow \uparrow} & S_{\downarrow \downarrow} | ||
| \end{pmatrix}\,. | ||
| These matrices describe the transmission of plane waves propagating into positive | ||
| z-direction, reflection of plane waves into the positive z-direction, reflection of | ||
| plane waves into negative z-direction, and the transmission of plane waves | ||
| propagating in negative z-direction, respectively. Each of those for matrices | ||
| contain different diffraction orders and different polarizations. The polarizations | ||
| can be in either helicity of parity basis. | ||
| The wave number :attr:`k0` and, if not vacuum, the material :attr:`material` are | ||
| also required. | ||
| Args: | ||
| smats (SMatrix): S-matrices. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (PlaneWaveBasisByComp): The basis for the S-matrices. | ||
| material (tuple, Material, optional): Material definition, if a tuple of length | ||
| two is specified, this refers to the materials above and below the S-matrix. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| """ | ||
| permute = OperatorAttributeSMatrices("permute") | ||
| translate = OperatorAttributeSMatrices("translate") | ||
| rotate = OperatorAttributeSMatrices("rotate") | ||
| changepoltype = OperatorAttributeSMatrices("changepoltype") | ||
| def __init__(self, smats, **kwargs): | ||
| """Initialization.""" | ||
| if len(smats) != 2 or len(smats[0]) != 2 or len(smats[1]) != 2: | ||
| raise ValueError("invalid shape of S-matrices") | ||
| if "material" in kwargs: | ||
| material = kwargs["material"] | ||
| del kwargs["material"] | ||
| elif hasattr(smats[0][0], "material"): | ||
| material = smats[0][0].material | ||
| elif hasattr(smats[1][1], "material"): | ||
| material = smats[1][1].material | ||
| if isinstance(material, tuple): | ||
| material = material[::-1] | ||
| elif hasattr(smats[0][1], "material"): | ||
| material = smats[0][1].material | ||
| elif hasattr(smats[1][0], "material"): | ||
| material = smats[1][0].material | ||
| else: | ||
| material = Material() | ||
| if isinstance(material, tuple): | ||
| ma, mb = material | ||
| else: | ||
| ma = mb = Material(material) | ||
| material = [[(ma, mb), (ma, ma)], [(mb, mb), (mb, ma)]] | ||
| modetype = [[("up", "up"), ("up", "down")], [("down", "up"), ("down", "down")]] | ||
| self._sms = [ | ||
| [ | ||
| SMatrix(s, material=m, modetype=t, **kwargs) | ||
| for s, m, t in zip(ar, mr, tr) | ||
| ] | ||
| for ar, mr, tr in zip(smats, material, modetype) | ||
| ] | ||
| self.material = self[0, 0].material | ||
| self.basis = self[0, 0].basis | ||
| self.k0 = self[0, 0].k0 | ||
| self.poltype = self[0, 0].poltype | ||
| for i in (self[1, 0], self[0, 1], self[1, 1]): | ||
| i.k0 = self.k0 | ||
| i.basis = self.basis | ||
| i.poltype = self.poltype | ||
| def __eq__(self, other): | ||
| return all( | ||
| (np.abs(self[i, j] - other[i][j]) < 1e-14).all() | ||
| for i in range(2) | ||
| for j in range(2) | ||
| ) | ||
| def __getitem__(self, key): | ||
| keys = {0: 0, "up": 0, 1: 1, "down": 1} | ||
| if key in keys: | ||
| return self._sms[keys[key]] | ||
| if not isinstance(key, tuple) or len(key) not in (1, 2): | ||
| raise KeyError(f"invalid key: '{key}'") | ||
| if len(key) == 1: | ||
| return self[key[0]] | ||
| key = tuple(keys[k] for k in key) | ||
| return self._sms[key[0]][key[1]] | ||
| def __len__(self): | ||
| return 2 | ||
| def __iter__(self): | ||
| return iter(self._sms) | ||
| @classmethod | ||
| def interface(cls, basis, k0, materials, poltype=None): | ||
| """Planar interface between two media. | ||
| Args: | ||
| basis (PlaneWaveBasisByComp): Basis definitions. | ||
| k0 (float): Wave number in vacuum | ||
| materials (Sequence[Material]): Material definitions. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| materials = tuple(map(Material, materials)) | ||
| ks = np.array([m.ks(k0) for m in materials]) | ||
| choice = basis.pol == 0 | ||
| kxs = basis.kx[choice], basis.kx[~choice] | ||
| kys = basis.ky[choice], basis.ky[~choice] | ||
| qs = np.zeros((2, 2, len(basis), len(basis)), complex) | ||
| if all(kxs[0] == kxs[1]) and all(kys[0] == kys[1]): | ||
| kzs = np.stack( | ||
| [ | ||
| m.kzs(k0, kxs[0][:, None], kys[0][:, None], np.array([[0, 1]])) | ||
| for m in materials | ||
| ], | ||
| -2, | ||
| ) | ||
| vals = fresnel(ks, kzs, [m.impedance for m in materials]) | ||
| qs[:, :, choice, choice] = np.moveaxis(vals[:, :, :, 0, 0], 0, -1) | ||
| qs[:, :, choice, ~choice] = np.moveaxis(vals[:, :, :, 0, 1], 0, -1) | ||
| qs[:, :, ~choice, choice] = np.moveaxis(vals[:, :, :, 1, 0], 0, -1) | ||
| qs[:, :, ~choice, ~choice] = np.moveaxis(vals[:, :, :, 1, 1], 0, -1) | ||
| else: | ||
| for i, (kx, ky, pol) in enumerate(basis): | ||
| for ii, (kx2, ky2, pol2) in enumerate(basis): | ||
| if kx != kx2 or ky != ky2: | ||
| continue | ||
| kzs = np.array([m.kzs(k0, kx, ky) for m in materials]) | ||
| vals = fresnel(ks, kzs, [m.impedance for m in materials]) | ||
| qs[:, :, ii, i] = vals[:, :, pol2, pol] | ||
| res = cls(qs, k0=k0, basis=basis, material=materials[::-1], poltype="helicity") | ||
| if poltype == "helicity": | ||
| return res | ||
| res = res.changepoltype(poltype) | ||
| res[0, 0][~np.eye(len(res), dtype=bool)] = 0 | ||
| res[0, 1][~np.eye(len(res), dtype=bool)] = 0 | ||
| res[1, 0][~np.eye(len(res), dtype=bool)] = 0 | ||
| res[1, 1][~np.eye(len(res), dtype=bool)] = 0 | ||
| return res | ||
| @classmethod | ||
| def slab(cls, thickness, basis, k0, materials, poltype=None): | ||
| """Slab of material. | ||
| A slab of material, defined by a thickness and three materials. Consecutive | ||
| slabs of material can be defined by `n` thicknesses and and `n + 2` material | ||
| parameters. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| basis (PlaneWaveBasisByComp): Basis definition. | ||
| tickness (Sequence[Float]): Thickness of the slab or the thicknesses of all | ||
| slabs in order from negative to positive z. | ||
| materials (Sequenze[Material]): Material definitions from negative to | ||
| positive z. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| try: | ||
| iter(thickness) | ||
| except TypeError: | ||
| thickness = [thickness] | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| res = cls.interface(basis, k0, materials[:2], poltype) | ||
| for d, ma, mb in zip(thickness, materials[1:-1], materials[2:]): | ||
| if np.ndim(d) == 0: | ||
| d = [0, 0, d] | ||
| x = cls.propagation(d, basis, k0, ma, poltype) | ||
| res = res.add(x) | ||
| res = res.add(cls.interface(basis, k0, (ma, mb), poltype)) | ||
| return res | ||
| @classmethod | ||
| def stack(cls, items): | ||
| """Stack of S-matrices. | ||
| Electromagnetically couple multiple S-matrices in the order given. Before | ||
| coupling it can be checked for matching materials and modes. | ||
| Args: | ||
| items (Sequence[SMatrix]): An array of S-matrices in their intended order | ||
| from negative to positive z. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| acc = items[0] | ||
| for item in items[1:]: | ||
| acc = acc.add(item) | ||
| return acc | ||
| @classmethod | ||
| def propagation(cls, r, basis, k0, material=Material(), poltype=None): | ||
| """S-matrix for the propagation along a distance. | ||
| This S-matrix translates the reference origin along `r`. | ||
| Args: | ||
| r (float, (3,)-array): Translation vector. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (PlaneWaveBasis): Basis definition. | ||
| material (Material, optional): Material definition. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| sup = translate( | ||
| r, basis=basis, k0=k0, material=material, modetype="up", poltype=poltype | ||
| ) | ||
| sdown = translate( | ||
| np.negative(r), | ||
| basis=basis, | ||
| k0=k0, | ||
| material=material, | ||
| modetype="down", | ||
| poltype=poltype, | ||
| ) | ||
| zero = np.zeros_like(sup) | ||
| material = Material(material) | ||
| return cls( | ||
| [[sup, zero], [zero, sdown]], | ||
| basis=basis, | ||
| k0=k0, | ||
| material=material, | ||
| poltype=poltype, | ||
| ) | ||
| @classmethod | ||
| def from_array(cls, tm, basis): | ||
| """S-matrix from an array of (cylindrical) T-matrices. | ||
| Create a S-matrix for a two-dimensional array of objects described by the | ||
| T-Matrix or an one-dimensional array of objects described by a cylindrical | ||
| T-matrix. | ||
| Args: | ||
| tm (TMatrix or TMatrixC): (Cylindrical) T-matrix to place in the array. | ||
| basis (PlaneWaveBasisByComp): Basis definition. | ||
| eta (float or complex, optional): Splitting parameter in the lattice sum. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| if isinstance(tm, TMatrixC): | ||
| basis = basis.permute(-1) | ||
| pu, pd = (tm.expandlattice(basis=basis, modetype=i) for i in ("up", "down")) | ||
| au, ad = (tm.expand.eval_inv(basis, i) for i in ("up", "down")) | ||
| eye = np.eye(len(basis)) | ||
| res = cls([[eye + pu @ au, pu @ ad], [pd @ au, eye + pd @ ad]]) | ||
| if isinstance(tm, TMatrixC): | ||
| res = res.permute() | ||
| return res | ||
| def add(self, sm): | ||
| """Couple another S-matrix on top of the current one. | ||
| See also :func:`treams.SMatrix.stack` for a function that does not change the | ||
| current S-matrix but creates a new one. | ||
| Args: | ||
| sm (SMatrix): S-matrix to add. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| dim = len(self.basis) | ||
| snew = [[None, None], [None, None]] | ||
| s_tmp = np.linalg.solve(np.eye(dim) - self[0, 1] @ sm[1][0], self[0, 0]) | ||
| snew[0][0] = sm[0][0] @ s_tmp | ||
| snew[1][0] = self[1, 0] + self[1, 1] @ sm[1][0] @ s_tmp | ||
| s_tmp = np.linalg.solve(np.eye(dim) - sm[1][0] @ self[0, 1], sm[1][1]) | ||
| snew[1][1] = self[1, 1] @ s_tmp | ||
| snew[0][1] = sm[0][1] + sm[0][0] @ self[0, 1] @ s_tmp | ||
| return SMatrices(snew) | ||
| def double(self, n=1): | ||
| """Double the S-matrix. | ||
| By default this function doubles the S-matrix but it can also create a | ||
| :math:`2^n`-fold repetition of itself: | ||
| Args: | ||
| n (int, optional): Number of times to double itself. Defaults to 1. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| res = self | ||
| for _ in range(n): | ||
| res = res.add(res) | ||
| return res | ||
| def illuminate(self, illu, illu2=None, /, *, smat=None): | ||
| """Field coefficients above and below the S-matrix. | ||
| Given an illumination defined by the coefficients of each incoming mode | ||
| calculate the coefficients for the outgoing field above and below the S-matrix. | ||
| If a second SMatrix is given, the field expansions in between are also | ||
| calculated. | ||
| Args: | ||
| illu (array-like): Illumination, if `modetype` is specified, the direction | ||
| will be chosen accordingly. | ||
| illu2 (array-like, optional): Second illumination. If used, the first | ||
| argument is taken to be coming from below and this one to be coming from | ||
| above. | ||
| smat (SMatrix, optional): Second S-matrix for the calculation of the | ||
| field expansion between two S-matrices. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| modetype = getattr(illu, "modetype", "up") | ||
| if isinstance(modetype, tuple): | ||
| modetype = modetype[max(-2, -len(tuple))] | ||
| illu2 = np.zeros(np.shape(illu)[-2:]) if illu2 is None else illu2 | ||
| if modetype == "down": | ||
| illu, illu2 = illu2, illu | ||
| if smat is None: | ||
| field_up = self[0, 0] @ illu + self[0, 1] @ illu2 | ||
| field_down = self[1, 0] @ illu + self[1, 1] @ illu2 | ||
| return field_up, field_down | ||
| stmp = np.eye(len(self.basis)) - self[0, 1] @ smat[1, 0] | ||
| field_in_up = np.linalg.solve( | ||
| stmp, self[0, 0] @ illu + self[0, 1] @ smat[1, 1] @ illu2 | ||
| ) | ||
| field_in_down = smat[1, 0] @ field_in_up + smat[1, 1] @ illu2 | ||
| field_up = smat[0, 1] @ illu2 + smat[0, 0] @ field_in_up | ||
| field_down = self[1, 0] @ illu + self[1, 1] @ field_in_down | ||
| return field_up, field_down, field_in_up, field_in_down | ||
| def tr(self, illu): | ||
| """Transmittance and reflectance. | ||
| Calculate the transmittance and reflectance for one S-matrix with the given | ||
| illumination and direction. | ||
| Args: | ||
| illu (complex, array_like): Expansion coefficients for the incoming light | ||
| Returns: | ||
| tuple | ||
| """ | ||
| modetype = getattr(illu, "modetype", "up") | ||
| if isinstance(modetype, tuple): | ||
| modetype = modetype[max(-2, -len(tuple))] | ||
| trans, refl = self.illuminate(illu) | ||
| material = self.material | ||
| if not isinstance(material, tuple): | ||
| material = material, material | ||
| paz = [poynting_avg_z(self.basis, self.k0, m, self.poltype) for m in material] | ||
| if modetype == "down": | ||
| trans, refl = refl, trans | ||
| paz.reverse() | ||
| illu = np.asarray(illu) | ||
| s_t = np.real(trans.conjugate().T @ paz[0][0] @ trans) | ||
| s_r = np.real(refl.conjugate().T @ paz[1][0] @ refl) | ||
| s_i = np.real(np.conjugate(illu).T @ paz[1][0] @ illu) | ||
| s_ir = np.real( | ||
| refl.conjugate().T @ paz[1][1] @ illu | ||
| - np.conjugate(illu).T @ paz[1][1] @ refl | ||
| ) | ||
| return s_t / (s_i + s_ir), s_r / (s_i + s_ir) | ||
| def cd(self, illu): | ||
| """Transmission and absorption circular dichroism. | ||
| Calculate the transmission and absorption CD for one S-matrix with the given | ||
| illumination and direction. | ||
| Args: | ||
| illu (complex, PhysicsArray): Expansion coefficients for the incoming light | ||
| Returns: | ||
| tuple | ||
| """ | ||
| minus, plus = self.basis.pol == 0, self.basis.pol == 1 | ||
| if self.poltype == "helicity": | ||
| illuopposite = np.zeros_like(illu) | ||
| illuopposite[minus] = illu[plus] | ||
| illuopposite[plus] = illu[minus] | ||
| else: | ||
| illuopposite = copy.deepcopy(illu) | ||
| illuopposite[minus] *= -1 | ||
| tm, rm = self.tr(illu) | ||
| tp, rp = self.tr(illuopposite) | ||
| return (tp - tm) / (tp + tm), (tp + rp - tm - rm) / (tp + rp + tm + rm) | ||
| def periodic(self): | ||
| r"""Periodic repetition of the S-matrix. | ||
| Transform the S-matrix to an infinite periodic arrangement of itself defined | ||
| by | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| S_{\uparrow \uparrow} & S_{\uparrow \downarrow} \\ | ||
| -S_{\downarrow \downarrow}^{-1} | ||
| S_{\downarrow \uparrow} S_{\uparrow \uparrow} & | ||
| S_{\downarrow \downarrow}^{-1} (\mathbb{1} - S_{\downarrow \uparrow} | ||
| S_{\uparrow \downarrow}) | ||
| \end{pmatrix}\,. | ||
| Returns: | ||
| complex, array_like | ||
| """ | ||
| dim = len(self.basis) | ||
| res = np.empty((2 * dim, 2 * dim), dtype=complex) | ||
| res[0:dim, 0:dim] = self[0, 0] | ||
| res[0:dim, dim:] = self[0, 1] | ||
| res[dim:, 0:dim] = -np.linalg.solve(self[1, 1], self[1, 0] @ self[0, 0]) | ||
| res[dim:, dim:] = np.linalg.solve( | ||
| self[1, 1], np.eye(dim) - self[1, 0] @ self[0, 1] | ||
| ) | ||
| return res | ||
| def bands_kz(self, az): | ||
| r"""Band structure calculation. | ||
| Calculate the band structure for the given S-matrix, assuming it is periodically | ||
| repeated along the z-axis. The function returns the z-components of the wave | ||
| vector :math:`k_z` and the corresponding eigenvectors :math:`v` of | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| S_{\uparrow \uparrow} & S_{\uparrow \downarrow} \\ | ||
| -S_{\downarrow \downarrow}^{-1} S_{\downarrow \uparrow} | ||
| S_{\uparrow \uparrow} & | ||
| S_{\downarrow \downarrow}^{-1} (\mathbb{1} - S_{\downarrow \uparrow} | ||
| S_{\uparrow \downarrow}) | ||
| \end{pmatrix} | ||
| \boldsymbol v | ||
| = | ||
| \mathrm{e}^{\mathrm{i}k_z a_z} | ||
| \boldsymbol v\,. | ||
| Args: | ||
| az (float): Lattice pitch along the z direction | ||
| Returns: | ||
| tuple | ||
| """ | ||
| w, v = np.linalg.eig(self.periodic()) | ||
| return -1j * np.log(w) / az, v | ||
| def __repr__(self): | ||
| return f"""{type(self).__name__}(..., | ||
| basis={self.basis}, | ||
| k0={self.k0}, | ||
| material={self.material}, | ||
| poltype='{self.poltype}', | ||
| )""" | ||
| def chirality_density(basis, k0, material=Material(), poltype=None, z=(0, 0)): | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| material = Material(material) | ||
| kx, ky, kz = basis.kvecs(k0, material) | ||
| k = material.ks(k0)[basis.pol] | ||
| re, im = np.real(kz), np.imag(kz) | ||
| prefuu = 1 + np.real((k * k - kz * 2j * im) / (k * k.conjugate())) | ||
| prefdu = 1 + np.real((k * k - kz * 2 * re) / (k * k.conjugate())) | ||
| if np.abs(z[1] - z[0]) > 1e-16: | ||
| prefuu *= np.sinh(re * (z[1] - z[0])) / (re * (z[1] - z[0])) | ||
| prefdd = np.exp(im * (z[1] + z[0])) * prefuu | ||
| prefuu *= np.exp(-im * (z[1] + z[0])) | ||
| prefdu *= ( | ||
| np.sin(re * (z[1] - z[0])) | ||
| * np.cos(re * (z[1] + z[0])) | ||
| / (re * (z[1] - z[0])) | ||
| ) | ||
| else: | ||
| prefdd = prefuu | ||
| if poltype == "helicity": | ||
| return ( | ||
| np.diag(prefuu * (2 * basis.pol - 1)), | ||
| np.diag(prefdd * (2 * basis.pol - 1)), | ||
| np.diag(2 * prefdu * (2 * basis.pol - 1)), | ||
| ) | ||
| if poltype == "parity": | ||
| where = ( | ||
| (kx[:, None] == kx) | ||
| & (ky[:, None] == ky) | ||
| & (basis.pol[:, None] != basis.pol) | ||
| ) | ||
| return where * prefuu, where * prefdd, where * 2 * prefdu | ||
| raise ValueError(f"invalid poltype: '{poltype}'") | ||
| def poynting_avg_z(basis, k0, material=Material(), poltype=None): | ||
| r"""Time-averaged z-component of the Poynting vector. | ||
| Calculate the time-averaged Poynting vector's z-component | ||
| .. math:: | ||
| \langle S_z \rangle | ||
| = \frac{1}{2} | ||
| \Re (\boldsymbol E \times \boldsymbol H^\ast) \boldsymbol{\hat{z}} | ||
| on one side of the S-matrix with the given coefficients. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| material = Material(material) | ||
| kx, ky, kzs = basis.kvecs(k0, material) | ||
| gamma = kzs / (material.ks(k0)[basis.pol] * material.impedance) | ||
| selection = (kx[:, None] == kx) & (ky[:, None] == ky) | ||
| if poltype == "parity": | ||
| pol = basis.pol | ||
| selection = selection & (pol[:, None] == pol) | ||
| a = selection * ((1 - pol) * gamma.conjugate() + pol * gamma) * 0.25 | ||
| b = selection * ((1 - pol) * gamma.conjugate() - pol * gamma) * 0.25 | ||
| return a, b | ||
| if poltype == "helicity": | ||
| pol = 2 * basis.pol - 1 | ||
| a = selection * (pol[:, None] * pol * gamma[:, None].conjugate() + gamma) * 0.25 | ||
| b = selection * (pol[:, None] * pol * gamma[:, None].conjugate() - gamma) * 0.25 | ||
| return a, b | ||
| raise ValueError(f"invalid poltype: '{poltype}'") |
| import warnings | ||
| import numpy as np | ||
| import treams._operators as op | ||
| import treams.special as sc | ||
| from treams import config | ||
| from treams._core import CylindricalWaveBasis as CWB | ||
| from treams._core import PhysicsArray | ||
| from treams._core import PlaneWaveBasisByComp as PWBC | ||
| from treams._core import PlaneWaveBasisByUnitVector as PWBUV | ||
| from treams._core import SphericalWaveBasis as SWB | ||
| from treams._material import Material | ||
| from treams.coeffs import mie, mie_cyl | ||
| from treams.util import AnnotationError | ||
| class _Interaction: | ||
| def __init__(self): | ||
| self._obj = self._objtype = None | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| self._objtype = objtype | ||
| return self | ||
| def __call__(self): | ||
| basis = self._obj.basis | ||
| return ( | ||
| np.eye(self._obj.shape[-1]) - self._obj @ op.Expand(basis, "singular").inv | ||
| ) | ||
| def solve(self): | ||
| return np.linalg.solve(self(), self._obj) | ||
| class _LatticeInteraction: | ||
| def __init__(self): | ||
| self._obj = self._objtype = None | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| self._objtype = objtype | ||
| return self | ||
| def __call__(self, lattice, kpar, *, eta=0): | ||
| return np.eye(self._obj.shape[-1]) - self._obj @ op.ExpandLattice( | ||
| lattice=lattice, kpar=kpar, eta=eta | ||
| ) | ||
| def solve(self, lattice, kpar, *, eta=0): | ||
| return np.linalg.solve(self(lattice, kpar, eta=eta), self._obj) | ||
| class TMatrix(PhysicsArray): | ||
| """T-matrix with a spherical basis. | ||
| The T-matrix is square relating incident (regular) fields | ||
| :func:`treams.special.vsw_rA` (helical polarizations) or | ||
| :func:`treams.special.vsw_rN` and :func:`treams.special.vsw_rM` (parity | ||
| polarizations) to the corresponding scattered fields :func:`treams.special.vsw_A` or | ||
| :func:`treams.special.vsw_N` and :func:`treams.special.vsw_M`. The modes themselves | ||
| are defined in :attr:`basis`, the polarization type in :attr:`poltype`. Also, the | ||
| wave number :attr:`k0` and, if not vacuum, the material :attr:`material` are | ||
| specified. | ||
| Args: | ||
| arr (float or complex, array-like): T-matrix itself. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (SphericalWaveBasis, optional): Basis definition. | ||
| material (Material, optional): Embedding material, defaults to vacuum. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| lattice (Lattice, optional): Lattice definition. If specified the T-Matrix is | ||
| assumed to be periodically repeated in the defined lattice. | ||
| kpar (list, optional): Phase factor for the periodic T-Matrix. | ||
| """ | ||
| interaction = _Interaction() | ||
| latticeinteraction = _LatticeInteraction() | ||
| def _check(self): | ||
| """Fill in default values or raise errors for missing attributes.""" | ||
| super()._check() | ||
| shape = np.shape(self) | ||
| if len(shape) != 2 or shape[0] != shape[1]: | ||
| raise AnnotationError(f"invalid shape: '{shape}'") | ||
| if not isinstance(self.k0, (int, float, np.floating, np.integer)): | ||
| raise AnnotationError("invalid k0") | ||
| if self.poltype is None: | ||
| self.poltype = config.POLTYPE | ||
| if self.poltype not in ("parity", "helicity"): | ||
| raise AnnotationError("invalid poltype") | ||
| modetype = self.modetype | ||
| if modetype is None or ( | ||
| modetype[0] in (None, "singular") and modetype[1] in (None, "regular") | ||
| ): | ||
| self.modetype = ("singular", "regular") | ||
| else: | ||
| raise AnnotationError("invalid modetype") | ||
| if self.basis is None: | ||
| self.basis = SWB.default(SWB.defaultlmax(shape[0])) | ||
| if self.material is None: | ||
| self.material = Material() | ||
| @property | ||
| def ks(self): | ||
| """Wave numbers (in medium). | ||
| The wave numbers for both polarizations. | ||
| """ | ||
| return self.material.ks(self.k0) | ||
| @classmethod | ||
| def sphere(cls, lmax, k0, radii, materials, poltype=None): | ||
| """T-Matrix of a (multi-layered) sphere. | ||
| Construct the T-matrix of the given order and material for a sphere. The object | ||
| can also consist of multiple concentric spherical shells with an arbitrary | ||
| number of layers. The calculation is always done in helicity basis. | ||
| Args: | ||
| lmax (int): Positive integer for the maximum degree of the T-matrix. | ||
| k0 (float): Wave number in vacuum. | ||
| radii (float or array): Radii from inside to outside of the sphere. For a | ||
| simple sphere the radius can be given as a single number, for a multi- | ||
| layered sphere it is a list of increasing radii for all shells. | ||
| material (list[Material]): The material parameters from the inside to the | ||
| outside. The last material in the list specifies the embedding medium. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| TMatrix | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| materials = [Material(m) for m in materials] | ||
| radii = np.atleast_1d(radii) | ||
| if radii.size != len(materials) - 1: | ||
| raise ValueError("incompatible lengths of radii and materials") | ||
| dim = SWB.defaultdim(lmax) | ||
| tmat = np.zeros((dim, dim), np.complex128) | ||
| for l in range(1, lmax + 1): # noqa: E741 | ||
| miecoeffs = mie(l, k0 * radii, *zip(*materials)) | ||
| pos = SWB.defaultdim(l - 1) | ||
| for i in range(2 * l + 1): | ||
| tmat[ | ||
| pos + 2 * i : pos + 2 * i + 2, pos + 2 * i : pos + 2 * i + 2 | ||
| ] = miecoeffs[::-1, ::-1] | ||
| res = cls( | ||
| tmat, | ||
| k0=k0, | ||
| basis=SWB.default(lmax), | ||
| material=materials[-1], | ||
| poltype="helicity", | ||
| ) | ||
| if poltype == "helicity": | ||
| return res | ||
| res = res.changepoltype(poltype) | ||
| res[~np.eye(len(res), dtype=bool)] = 0 | ||
| return res | ||
| @classmethod | ||
| def cluster(cls, tmats, positions): | ||
| r"""Block-diagonal T-matrix of multiple objects. | ||
| Construct the initial block-diagonal T-matrix for a cluster of objects. The | ||
| T-matrices in the list are placed together into a block-diagonal matrix and the | ||
| complete (local) basis is defined based on the individual T-matrices and their | ||
| bases together with the defined positions. In mathematical terms the matrix | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| T_0 & 0 & \dots & 0 \\ | ||
| 0 & T_1 & \ddots & \vdots \\ | ||
| \vdots & \ddots & \ddots & 0 \\ | ||
| 0 & \dots & 0 & T_{N-1} \\ | ||
| \end{pmatrix} | ||
| is created from the list of T-matrices :math:`(T_0, \dots, T_{N-1})`. Only | ||
| T-matrices of the same wave number, embedding material, and polarization type | ||
| can be combined. | ||
| Args: | ||
| tmats (Sequence): List of T-matrices. | ||
| positions (array): The positions of all individual objects in the cluster. | ||
| Returns: | ||
| TMatrix | ||
| """ | ||
| for tm in tmats: | ||
| if not tm.basis.isglobal: | ||
| raise ValueError("global basis required") | ||
| positions = np.array(positions) | ||
| if len(tmats) < positions.shape[0]: | ||
| warnings.warn("specified more positions than T-matrices") | ||
| elif len(tmats) > positions.shape[0]: | ||
| raise ValueError( | ||
| f"'{len(tmats)}' T-matrices " | ||
| f"but only '{positions.shape[0]}' positions given" | ||
| ) | ||
| mat = tmats[0].material | ||
| k0 = tmats[0].k0 | ||
| poltype = tmats[0].poltype | ||
| modes = [], [], [] | ||
| pidx = [] | ||
| dim = sum(tmat.shape[0] for tmat in tmats) | ||
| tres = np.zeros((dim, dim), complex) | ||
| i = 0 | ||
| for j, tm in enumerate(tmats): | ||
| if tm.material != mat: | ||
| raise ValueError(f"incompatible materials: '{mat}' and '{tm.material}'") | ||
| if tm.k0 != k0: | ||
| raise ValueError(f"incompatible k0: '{k0}' and '{tm.k0}'") | ||
| if tm.poltype != poltype: | ||
| raise ValueError(f"incompatible modetypes: '{poltype}', '{tm.poltype}'") | ||
| dim = tm.shape[0] | ||
| for m, n in zip(modes, tm.basis.lms): | ||
| m.extend(list(n)) | ||
| pidx += [j] * dim | ||
| tres[i : i + dim, i : i + dim] = tm | ||
| i += dim | ||
| basis = SWB(zip(pidx, *modes), positions) | ||
| return cls(tres, k0=k0, material=mat, basis=basis, poltype=poltype) | ||
| @property | ||
| def isglobal(self): | ||
| """Test if a T-matrix is global. | ||
| A T-matrix is considered global, when its basis refers to only a single point | ||
| and it is not placed periodically in a lattice. | ||
| """ | ||
| return self.basis.isglobal and self.lattice is None and self.kpar is None | ||
| @property | ||
| def xs_ext_avg(self): | ||
| r"""Rotation and polarization averaged extinction cross section. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \sigma_\mathrm{ext} \rangle | ||
| = -2 \pi \sum_{slm} \frac{\Re(T_{slm,slm})}{k_s^2} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s`. It is only implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| if not self.material.ischiral: | ||
| k = self.ks[0] | ||
| res = -2 * np.pi * self.trace().real / (k * k) | ||
| else: | ||
| res = 0 | ||
| modetype = self.modetype | ||
| del self.modetype | ||
| diag = np.diag(self) | ||
| self.modetype = modetype | ||
| for pol in [0, 1]: | ||
| choice = self.basis.pol == pol | ||
| k = self.ks[pol] | ||
| res += -2 * np.pi * diag[choice].sum().real / (k * k) | ||
| if res.imag == 0: | ||
| return res.real | ||
| return res | ||
| @property | ||
| def xs_sca_avg(self): | ||
| r"""Rotation and polarization averaged scattering cross section. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \sigma_\mathrm{sca} \rangle | ||
| = 2 \pi \sum_{slm} \sum_{s'l'm'} | ||
| \frac{|T_{slm,s'l'm'}|^2}{k_s^2} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s`. It is only implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| if not self.material.ischiral: | ||
| ks = self.ks[0] | ||
| else: | ||
| ks = self.ks[self.basis.pol, None] | ||
| re, im = self.real, self.imag | ||
| res = 2 * np.pi * np.sum((re * re + im * im) / (ks * ks)) | ||
| return res.real | ||
| @property | ||
| def cd(self): | ||
| r"""Circular dichroism (CD). | ||
| The CD is calculated as | ||
| .. math:: | ||
| CD | ||
| = \frac{\langle \sigma_\mathrm{abs} \rangle_+ | ||
| - \langle \sigma_\mathrm{abs} \rangle_-} | ||
| {\langle \sigma_\mathrm{abs} \rangle_+ | ||
| + \langle \sigma_\mathrm{abs} \rangle_-} | ||
| where :math:`\langle \sigma \rangle_s` is the rotationally averaged absorption | ||
| cross section under the illumination with the polarization :math:`s`. | ||
| It is only implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not (self.isglobal and self.poltype == "helicity" and self.material.isreal): | ||
| raise NotImplementedError | ||
| sel = np.array(self.basis.pol, bool) | ||
| re, im = self.real, self.imag | ||
| plus = -np.sum(re.diagonal()[sel]) / (self.ks[1] * self.ks[1]) | ||
| re_part = re[:, sel] / self.ks[self.basis.pol, None] | ||
| im_part = im[:, sel] / self.ks[self.basis.pol, None] | ||
| plus -= np.sum(re_part * re_part + im_part * im_part) | ||
| sel = ~sel | ||
| minus = -np.sum(re.diagonal()[sel]) / (self.ks[0] * self.ks[0]) | ||
| re_part = re[:, sel] / self.ks[self.basis.pol, None] | ||
| im_part = im[:, sel] / self.ks[self.basis.pol, None] | ||
| minus -= np.sum(re_part * re_part + im_part * im_part) | ||
| return np.real((plus - minus) / (plus + minus)) | ||
| @property | ||
| def chi(self): | ||
| """Electromagnetic chirality. | ||
| Only implemented for global T-matrices. | ||
| Returns: | ||
| float | ||
| """ | ||
| if not (self.isglobal and self.poltype == "helicity"): | ||
| raise NotImplementedError | ||
| sel = self.basis.pol == 0, self.basis.pol == 1 | ||
| spp = np.linalg.svd(self[np.ix_(sel[1], sel[1])], compute_uv=False) | ||
| spm = np.linalg.svd(self[np.ix_(sel[1], sel[0])], compute_uv=False) | ||
| smp = np.linalg.svd(self[np.ix_(sel[0], sel[1])], compute_uv=False) | ||
| smm = np.linalg.svd(self[np.ix_(sel[0], sel[0])], compute_uv=False) | ||
| plus = np.concatenate((np.asarray(spp), np.asarray(spm))) | ||
| minus = np.concatenate((np.asarray(smm), np.asarray(smp))) | ||
| return np.linalg.norm(plus - minus) / np.sqrt(np.sum(np.power(np.abs(self), 2))) | ||
| @property | ||
| def db(self): | ||
| """Duality breaking. | ||
| Only implemented for global T-matrices. | ||
| Returns: | ||
| float | ||
| """ | ||
| if not (self.isglobal and self.poltype == "helicity"): | ||
| raise NotImplementedError | ||
| sel = self.basis.pol == 0, self.basis.pol == 1 | ||
| tpm = np.asarray(self[np.ix_(sel[1], sel[0])]) | ||
| tmp = np.asarray(self[np.ix_(sel[0], sel[1])]) | ||
| return np.sum( | ||
| tpm.real * tpm.real | ||
| + tpm.imag * tpm.imag | ||
| + tmp.real * tmp.real | ||
| + tmp.imag * tmp.imag | ||
| ) / (np.sum(np.power(np.abs(self), 2))) | ||
| def xs(self, illu, flux=0.5): | ||
| r"""Scattering and extinction cross section. | ||
| Possible for all T-matrices (global and local) in non-absorbing embedding. The | ||
| values are calculated by | ||
| .. math:: | ||
| \sigma_\mathrm{sca} | ||
| = \frac{1}{2 I} | ||
| a_{slm}^\ast T_{s'l'm',slm}^\ast k_{s'}^{-2} C_{s'l'm',s''l''m''}^{(1)} | ||
| T_{s''l''m'',s'''l'''m'''} a_{s'''l'''m'''} \\ | ||
| \sigma_\mathrm{ext} | ||
| = \frac{1}{2 I} | ||
| a_{slm}^\ast k_s^{-2} T_{slm,s'l'm'} a_{s'l'm'} | ||
| where :math:`a_{slm}` are the expansion coefficients of the illumination, | ||
| :math:`T` is the T-matrix, :math:`C^{(1)}` is the (regular) translation | ||
| matrix and :math:`k_s` are the wave numbers in the medium. All repeated indices | ||
| are summed over. The incoming flux is :math:`I`. | ||
| Args: | ||
| illu (complex, array): Illumination coefficients | ||
| flux (optional): Ingoing flux corresponding to the illumination. Used for | ||
| the result's normalization. The flux is given in units of | ||
| :math:`\frac{\text{V}^2}{{l^2}} \frac{1}{Z_0 Z}` where :math:`l` is the | ||
| unit of length used in the wave number (and positions). A plane wave | ||
| has the flux `0.5` in this normalization, which is used as default. | ||
| Returns: | ||
| tuple[float] | ||
| """ | ||
| if not self.material.isreal: | ||
| raise NotImplementedError | ||
| illu = PhysicsArray(illu) | ||
| illu_basis = illu.basis | ||
| illu_basis = illu_basis[-2] if isinstance(illu_basis, tuple) else illu_basis | ||
| if not isinstance(illu_basis, SWB): | ||
| illu = illu.expand(self.basis) | ||
| p = self @ illu | ||
| p_invksq = p * np.power(self.ks[self.basis.pol], -2) | ||
| del illu.modetype | ||
| return ( | ||
| 0.5 * np.real(p.conjugate().T @ p_invksq.expand(p.basis)) / flux, | ||
| -0.5 * np.real(illu.conjugate().T @ p_invksq) / flux, | ||
| ) | ||
| def valid_points(self, grid, radii): | ||
| """Points on the grid where the expansion is valid. | ||
| The expansion of the electromagnetic field is valid outside of the | ||
| circumscribing spheres of each object. From a given set of coordinates mark | ||
| those that are outside of the given radii. | ||
| Args: | ||
| grid (array-like): Points to assess. The last dimension needs length three | ||
| and corresponds to the Cartesian coordinates. | ||
| radii (Sequence[float]): Radii of the circumscribing spheres. Each radius | ||
| corresponds to a position of the basis. | ||
| Returns: | ||
| array | ||
| """ | ||
| grid = np.asarray(grid) | ||
| if grid.shape[-1] != 3: | ||
| raise ValueError("invalid grid") | ||
| if len(radii) != len(self.basis.positions): | ||
| raise ValueError("invalid length of 'radii'") | ||
| res = np.ones(grid.shape[:-1], bool) | ||
| for r, p in zip(radii, self.basis.positions): | ||
| res &= np.sum(np.power(grid - p, 2), axis=-1) > r * r | ||
| return res | ||
| def __getitem__(self, key): | ||
| if isinstance(key, SWB): | ||
| key = np.array([self.basis.index(i) for i in key]) | ||
| key = (key[:, None], key) | ||
| return super().__getitem__(key) | ||
| class TMatrixC(PhysicsArray): | ||
| """T-matrix with a cylindrical basis. | ||
| The T-matrix is square relating incident (regular) fields | ||
| :func:`treams.special.vcw_rA` (helical polarizations) or | ||
| :func:`treams.special.vcw_rN` and :func:`treams.special.vcw_rM` (parity | ||
| polarizations) to the corresponding scattered fields :func:`treams.special.vcw_A` or | ||
| :func:`treams.special.vcw_N` and :func:`treams.special.vcw_M`. The modes themselves | ||
| are defined in :attr:`basis`, the polarization type in :attr:`poltype`. Also, the | ||
| wave number :attr:`k0` and, if not vacuum, the material :attr:`material` are | ||
| specified. | ||
| Args: | ||
| arr (float or complex, array-like): T-matrix itself. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (SphericalWaveBasis, optional): Basis definition. | ||
| material (Material, optional): Embedding material, defaults to vacuum. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| lattice (Lattice, optional): Lattice definition. If specified the T-Matrix is | ||
| assumed to be periodically repeated in the defined lattice. | ||
| kpar (list, optional): Phase factor for the periodic T-Matrix. | ||
| """ | ||
| interaction = _Interaction() | ||
| latticeinteraction = _LatticeInteraction() | ||
| def _check(self): | ||
| """Fill in default values or raise errors for missing attributes.""" | ||
| super()._check() | ||
| shape = np.shape(self) | ||
| if len(shape) != 2 or shape[0] != shape[1]: | ||
| raise AnnotationError(f"invalid shape: '{shape}'") | ||
| if not isinstance(self.k0, (int, float, np.floating, np.integer)): | ||
| raise AnnotationError("invalid k0") | ||
| if self.poltype is None: | ||
| self.poltype = config.POLTYPE | ||
| if self.poltype not in ("parity", "helicity"): | ||
| raise AnnotationError("invalid poltype") | ||
| modetype = self.modetype | ||
| if modetype is None or ( | ||
| modetype[0] in (None, "singular") and modetype[1] in (None, "regular") | ||
| ): | ||
| self.modetype = ("singular", "regular") | ||
| else: | ||
| raise AnnotationError("invalid modetype") | ||
| if self.basis is None: | ||
| self.basis = CWB.default([0], CWB.defaultmmax(shape[0])) | ||
| if self.material is None: | ||
| self.material = Material() | ||
| @property | ||
| def ks(self): | ||
| """Wave numbers (in medium). | ||
| The wave numbers for both polarizations. | ||
| """ | ||
| return self.material.ks(self.k0) | ||
| @property | ||
| def krhos(self): | ||
| r"""Radial part of the wave. | ||
| Calculate :math:`\sqrt{k^2 - k_z^2}`, where :math:`k` is the wave number in the | ||
| medium for each illumination | ||
| Returns: | ||
| Sequence[complex] | ||
| """ | ||
| return self.material.krhos(self.k0, self.basis.kz, self.basis.pol) | ||
| @classmethod | ||
| def cylinder(cls, kzs, mmax, k0, radii, materials): | ||
| """T-Matrix of a (multi-layered) cylinder. | ||
| Construct the T-matrix of the given order and material for an infinitely | ||
| extended cylinder. The object can also consist of multiple concentric | ||
| cylindrical shells with an arbitrary number of layers. The calculation is always | ||
| done in helicity basis. | ||
| Args: | ||
| kzs (float, array_like): Z component of the cylindrical wave. | ||
| mmax (int): Positive integer for the maximum order of the T-matrix. | ||
| k0 (float): Wave number in vacuum. | ||
| radii (float or array): Radii from inside to outside of the cylinder. For a | ||
| simple cylinder the radius can be given as a single number, for a multi- | ||
| layered cylinder it is a list of increasing radii for all shells. | ||
| material (list[Material]): The material parameters from the inside to the | ||
| outside. The last material in the list specifies the embedding medium. | ||
| Returns: | ||
| T-Matrix object | ||
| """ | ||
| materials = [Material(m) for m in materials] | ||
| kzs = np.atleast_1d(kzs) | ||
| radii = np.atleast_1d(radii) | ||
| if radii.size != len(materials) - 1: | ||
| raise ValueError("incompatible lengths of radii and materials") | ||
| dim = CWB.defaultdim(len(kzs), mmax) | ||
| tmat = np.zeros((dim, dim), np.complex128) | ||
| idx = 0 | ||
| for kz in kzs: | ||
| for m in range(-mmax, mmax + 1): | ||
| miecoeffs = mie_cyl(kz, m, k0, radii, *zip(*materials)) | ||
| tmat[idx : idx + 2, idx : idx + 2] = miecoeffs[::-1, ::-1] | ||
| idx += 2 | ||
| return cls(tmat, k0=k0, basis=CWB.default(kzs, mmax), material=materials[-1]) | ||
| @classmethod | ||
| def cluster(cls, tmats, positions): | ||
| r"""Block-diagonal T-matrix of multiple objects. | ||
| Construct the initial block-diagonal T-matrix for a cluster of objects. The | ||
| T-matrices in the list are placed together into a block-diagonal matrix and the | ||
| complete (local) basis is defined based on the individual T-matrices and their | ||
| bases together with the defined positions. In mathematical terms the matrix | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| T_0 & 0 & \dots & 0 \\ | ||
| 0 & T_1 & \ddots & \vdots \\ | ||
| \vdots & \ddots & \ddots & 0 \\ | ||
| 0 & \dots & 0 & T_{N-1} \\ | ||
| \end{pmatrix} | ||
| is created from the list of T-matrices :math:`(T_0, \dots, T_{N-1})`. Only | ||
| T-matrices of the same wave number, embedding material, and polarization type | ||
| can be combined. | ||
| Args: | ||
| tmats (Sequence): List of T-matrices. | ||
| positions (array): The positions of all individual objects in the cluster. | ||
| Returns: | ||
| TMatrix | ||
| """ | ||
| for tm in tmats: | ||
| if not tm.basis.isglobal: | ||
| raise ValueError("global basis required") | ||
| positions = np.array(positions) | ||
| if len(tmats) < positions.shape[0]: | ||
| warnings.warn("specified more positions than T-matrices") | ||
| elif len(tmats) > positions.shape[0]: | ||
| raise ValueError( | ||
| f"'{len(tmats)}' T-matrices " | ||
| f"but only '{positions.shape[0]}' positions given" | ||
| ) | ||
| mat = tmats[0].material | ||
| k0 = tmats[0].k0 | ||
| poltype = tmats[0].poltype | ||
| modes = [], [], [] | ||
| pidx = [] | ||
| dim = sum(tmat.shape[0] for tmat in tmats) | ||
| tres = np.zeros((dim, dim), complex) | ||
| i = 0 | ||
| for j, tm in enumerate(tmats): | ||
| if tm.material != mat: | ||
| raise ValueError(f"incompatible materials: '{mat}' and '{tm.material}'") | ||
| if tm.k0 != k0: | ||
| raise ValueError(f"incompatible k0: '{k0}' and '{tm.k0}'") | ||
| if tm.poltype != poltype: | ||
| raise ValueError(f"incompatible modetypes: '{poltype}', '{tm.poltype}'") | ||
| dim = tm.shape[0] | ||
| for m, n in zip(modes, tm.basis.zms): | ||
| m.extend(list(n)) | ||
| pidx += [j] * dim | ||
| tres[i : i + dim, i : i + dim] = tm | ||
| i += dim | ||
| basis = CWB(zip(pidx, *modes), positions) | ||
| return cls(tres, k0=k0, material=mat, basis=basis, poltype=poltype) | ||
| @classmethod | ||
| def from_array(cls, tm, basis, *, eta=0): | ||
| """1d array of spherical T-matrices.""" | ||
| return cls( | ||
| (tm @ op.Expand(basis).inv).expandlattice(basis=basis, eta=eta), | ||
| lattice=tm.lattice, | ||
| kpar=tm.kpar, | ||
| ) | ||
| @property | ||
| def xw_ext_avg(self): | ||
| r"""Rotation and polarization averaged extinction cross width. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \lambda_\mathrm{ext} \rangle | ||
| = -\frac{2 \pi}{n_{k_z}} \sum_{sk_zm} \frac{\Re(T_{sk_zm,sk_zm})}{k_s} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s` and :math:`n_{k_z}` is the number of wave components | ||
| :math:`k_z` included in the T-matrix. The average is taken over all given | ||
| z-components of the wave vector and rotations around the z-axis. It is only | ||
| implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| nk = np.unique(self.basis.kz).size | ||
| if not self.material.ischiral: | ||
| res = -2 * np.real(np.trace(self)) / (self.ks[0] * nk) | ||
| else: | ||
| res = 0 | ||
| modetype = self.modetype | ||
| del self.modetype | ||
| diag = np.diag(self) | ||
| self.modetype = modetype | ||
| for pol in [0, 1]: | ||
| choice = self.basis.pol == pol | ||
| res += -2 * np.real(diag[choice].sum()) / (self.ks[pol] * nk) | ||
| if res.imag == 0: | ||
| return res.real | ||
| return res | ||
| @property | ||
| def xw_sca_avg(self): | ||
| r"""Rotation and polarization averaged scattering cross width. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \lambda_\mathrm{sca} \rangle | ||
| = \frac{2 \pi}{n_{k_z}} \sum_{sk_zm} \sum_{s'{k_z}'m'} | ||
| \frac{|T_{sk_zm,s'{k_z}'m'}|^2}{k_s} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s`. and :math:`n_{k_z}` is the number of wave components | ||
| :math:`k_z` included in the T-matrix. The average is taken over all given | ||
| z-components of the wave vector and rotations around the z-axis. It is only | ||
| implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| if not self.material.ischiral: | ||
| ks = self.ks[0] | ||
| else: | ||
| ks = self.ks[self.basis.pol, None] | ||
| re, im = self.real, self.imag | ||
| nk = np.unique(self.basis.kz).size | ||
| res = 2 * np.sum((re * re + im * im) / (ks * nk)) | ||
| return res.real | ||
| @property | ||
| def isglobal(self): | ||
| """Test if a T-matrix is global. | ||
| A T-matrix is considered global, when its basis refers to only a single point | ||
| and it is not placed periodically in a lattice. | ||
| """ | ||
| return self.basis.isglobal and self.lattice is None and self.kpar is None | ||
| def xw(self, illu, flux=0.5): | ||
| r"""Scattering and extinction cross width. | ||
| Possible for all T-matrices (global and local) in non-absorbing embedding. The | ||
| values are calculated by | ||
| .. math:: | ||
| \lambda_\mathrm{sca} | ||
| = \frac{1}{2 I} | ||
| a_{sk_zm}^\ast T_{s'{k_z}'m',sk_zm}^\ast k_{s'}^{-2} | ||
| C_{s'l'm',s''l''m''}^{(1)} | ||
| T_{s''l''m'',s'''l'''m'''} a_{s'''l'''m'''} \\ | ||
| \sigma_\mathrm{ext} | ||
| = \frac{1}{2 I} | ||
| a_{slm}^\ast k_s^{-2} T_{slm,s'l'm'} a_{s'l'm'} | ||
| where :math:`a_{slm}` are the expansion coefficients of the illumination, | ||
| :math:`T` is the T-matrix, :math:`C^{(1)}` is the (regular) translation | ||
| matrix and :math:`k_s` are the wave numbers in the medium. All repeated indices | ||
| are summed over. The incoming flux is :math:`I`. | ||
| Args: | ||
| illu (complex, array): Illumination coefficients | ||
| flux (optional): Ingoing flux corresponding to the illumination. Used for | ||
| the result's normalization. The flux is given in units of | ||
| :math:`\frac{\text{V}^2}{{l^2}} \frac{1}{Z_0 Z}` where :math:`l` is the | ||
| unit of length used in the wave number (and positions). A plane wave | ||
| has the flux `0.5` in this normalization, which is used as default. | ||
| Returns: | ||
| tuple[float] | ||
| """ | ||
| if not self.material.isreal: | ||
| raise NotImplementedError | ||
| illu = PhysicsArray(illu) | ||
| illu_basis = illu.basis | ||
| illu_basis = illu_basis[-2] if isinstance(illu_basis, tuple) else illu_basis | ||
| if not isinstance(illu_basis, CWB): | ||
| illu = illu.expand(self.basis) | ||
| p = self @ illu | ||
| p_invk = p / self.ks[self.basis.pol] | ||
| del illu.modetype | ||
| return ( | ||
| 2 * np.real(p.conjugate().T @ p_invk.expand(p.basis)) / flux, | ||
| -2 * np.real(illu.conjugate().T @ p_invk) / flux, | ||
| ) | ||
| def valid_points(self, grid, radii): | ||
| grid = np.asarray(grid) | ||
| if grid.shape[-1] not in (2, 3): | ||
| raise ValueError("invalid grid") | ||
| if len(radii) != len(self.basis.positions): | ||
| raise ValueError("invalid length of 'radii'") | ||
| res = np.ones(grid.shape[:-1], bool) | ||
| for r, p in zip(radii, self.basis.positions): | ||
| res &= np.sum(np.power(grid[..., :2] - p[:2], 2), axis=-1) > r * r | ||
| return res | ||
| def __getitem__(self, key): | ||
| if isinstance(key, CWB): | ||
| key = np.array([self.basis.index(i) for i in key]) | ||
| key = (key[:, None], key) | ||
| return super().__getitem__(key) | ||
| def _plane_wave_partial( | ||
| kpar, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| if basis is None: | ||
| basis = PWBC.default([kpar]) | ||
| if pol in (0, -1): | ||
| pol = [1, 0] | ||
| elif pol == 1: | ||
| pol = [0, 1] | ||
| elif len(pol) == 3: | ||
| modetype = "up" if modetype is None else modetype | ||
| if modetype not in ("up", "down"): | ||
| raise ValueError(f"invalid 'modetype': {modetype}") | ||
| kvecs = np.array(basis.kvecs(k0, material, modetype)) | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| if poltype == "parity": | ||
| kvec = kvecs[:, basis.index((kpar[0], kpar[1], 0))] | ||
| pol = [ | ||
| -sc.vpw_M(*kvec, 0, 0, 0) @ pol, | ||
| sc.vpw_N(*kvec, 0, 0, 0) @ pol, | ||
| ] | ||
| elif poltype == "helicity": | ||
| kvec = kvecs[ | ||
| :, | ||
| [ | ||
| basis.index((kpar[0], kpar[1], 1)), | ||
| basis.index((kpar[0], kpar[1], 0)), | ||
| ], | ||
| ] | ||
| pol = sc.vpw_A(*kvec, 0, 0, 0, [1, 0]) @ pol | ||
| else: | ||
| raise ValueError(f"invalid 'poltype': {poltype}") | ||
| res = [pol[x[2]] * (np.abs(np.array(kpar) - x[:2]) < 1e-14).all() for x in basis] | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) | ||
| def _plane_wave( | ||
| kvec, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| if basis is None: | ||
| basis = PWBUV.default([kvec]) | ||
| norm = np.sqrt(np.sum(np.power(kvec, 2))) | ||
| qvec = kvec / norm | ||
| if pol in (0, -1): | ||
| pol = [1, 0] | ||
| elif pol == 1: | ||
| pol = [0, 1] | ||
| elif len(pol) == 3: | ||
| if None not in (k0, material): | ||
| kvec = Material(material).ks(k0) * qvec[:, None] | ||
| else: | ||
| kvec = qvec | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| if poltype == "parity": | ||
| pol = [ | ||
| -sc.vpw_M(*kvec[:, 0], 0, 0, 0) @ pol, | ||
| sc.vpw_N(*kvec[:, 1], 0, 0, 0) @ pol, | ||
| ] | ||
| elif poltype == "helicity": | ||
| pol = sc.vpw_A(*kvec, 0, 0, 0, [1, 0]) @ pol | ||
| else: | ||
| raise ValueError(f"invalid 'poltype': {poltype}") | ||
| res = [pol[x[3]] * (np.abs(qvec - x[:3]) < 1e-14).all() for x in basis] | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) | ||
| def plane_wave( | ||
| kvec, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| """Array describing a plane wave. | ||
| Args: | ||
| kvec (Sequence): Wave vector. | ||
| pol (int or Sequence): Polarization index (see | ||
| :ref:`params:Polarizations`) to have a unit amplitude wave of the | ||
| corresponding wave, two values to specify the amplitude for each | ||
| polarization, or three values in a sequence to specify the Cartesian | ||
| electric field components. In the latter case, if the electric field has | ||
| longitudinal components they are neglected. | ||
| basis (PlaneWaveBasis, optional): Basis definition. | ||
| k0 (float, optional): Wave number in vacuum. | ||
| material (Material, optional): Material definition. | ||
| modetype (str, optional): Mode type (see :ref:`params:Mode types`). | ||
| poltype (str, optional): Polarization type (see | ||
| :ref:`params:Polarizations`). | ||
| """ | ||
| if len(kvec) == 2: | ||
| return _plane_wave_partial( | ||
| kvec, | ||
| pol, | ||
| k0=k0, | ||
| basis=basis, | ||
| material=material, | ||
| modetype=modetype, | ||
| poltype=poltype, | ||
| ) | ||
| if len(kvec) == 3: | ||
| return _plane_wave( | ||
| kvec, | ||
| pol, | ||
| k0=k0, | ||
| basis=basis, | ||
| material=material, | ||
| modetype=modetype, | ||
| poltype=poltype, | ||
| ) | ||
| raise ValueError(f"invalid length of 'kvec': {len(kvec)}") | ||
| def plane_wave_angle(theta, phi, pol, **kwargs): | ||
| qvec = [np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta)] | ||
| return plane_wave(qvec, pol, **kwargs) | ||
| def spherical_wave( | ||
| l, # noqa: E741 | ||
| m, | ||
| pol, | ||
| *, | ||
| k0=None, | ||
| basis=None, | ||
| material=None, | ||
| modetype=None, | ||
| poltype=None, | ||
| ): | ||
| if basis is None: | ||
| basis = SWB.default(l) | ||
| if not basis.isglobal: | ||
| raise ValueError("basis must be global") | ||
| res = [0] * len(basis) | ||
| res[basis.index((0, l, m, pol))] = 1 | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) | ||
| def cylindrical_wave( | ||
| kz, m, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| if basis is None: | ||
| basis = CWB.default([kz], abs(m)) | ||
| if not basis.isglobal: | ||
| raise ValueError("basis must be global") | ||
| res = [0] * len(basis) | ||
| res[basis.index((0, kz, m, pol))] = 1 | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
+106
| import itertools | ||
| import numpy as np | ||
| import scipy.integrate | ||
| import treams.special as sc | ||
| def _j_real(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.real( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_A(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def _j_imag(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.imag( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_A(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def _rj_real(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.real( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_rA(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def _rj_imag(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.imag( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_rA(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def qmat(r, dr, ks, zs, out, in_=None, singular=True): | ||
| l_out, m_out, pol_out = out | ||
| in_ = out if in_ is None else in_ | ||
| l_in, m_in, pol_in = in_ | ||
| if singular: | ||
| fr = _j_real | ||
| fi = _j_imag | ||
| else: | ||
| fr = _rj_real | ||
| fi = _rj_imag | ||
| size_out = len(l_out) | ||
| size_in = len(l_in) | ||
| res = np.zeros((size_out, size_in), complex) | ||
| for i, j in itertools.product(range(size_out), range(size_in)): | ||
| if m_out[i] != m_in[j]: | ||
| continue | ||
| res[i, j] = ( | ||
| scipy.integrate.quad( | ||
| fr(l_out[i], l_in[j], m_out[i], r, dr, pol_out[i], pol_in[j], *ks, *zs), | ||
| 0, | ||
| np.pi, | ||
| )[0] | ||
| + 1j | ||
| * scipy.integrate.quad( | ||
| fi(l_out[i], l_in[j], m_out[i], r, dr, pol_out[i], pol_in[j], *ks, *zs), | ||
| 0, | ||
| np.pi, | ||
| )[0] | ||
| ) | ||
| return res |
+471
| """Loading and storing data. | ||
| Most functions rely on at least one of the external packages `h5py` or `gmsh`. | ||
| """ | ||
| import sys | ||
| import uuid as _uuid | ||
| from importlib.metadata import version | ||
| import numpy as np | ||
| import treams | ||
| try: | ||
| import h5py | ||
| except ImportError: | ||
| h5py = None | ||
| LENGTHS = { | ||
| "ym": 1e-24, | ||
| "zm": 1e-21, | ||
| "am": 1e-18, | ||
| "fm": 1e-15, | ||
| "pm": 1e-12, | ||
| "nm": 1e-9, | ||
| "um": 1e-6, | ||
| "µm": 1e-6, | ||
| "mm": 1e-3, | ||
| "cm": 1e-2, | ||
| "dm": 1e-1, | ||
| "m": 1, | ||
| "dam": 1e1, | ||
| "hm": 1e2, | ||
| "km": 1e3, | ||
| "Mm": 1e6, | ||
| "Gm": 1e9, | ||
| "Tm": 1e12, | ||
| "Pm": 1e15, | ||
| "Em": 1e18, | ||
| "Zm": 1e21, | ||
| "Ym": 1e24, | ||
| } | ||
| INVLENGTHS = { | ||
| r"ym^{-1}": 1e24, | ||
| r"zm^{-1}": 1e21, | ||
| r"am^{-1}": 1e18, | ||
| r"fm^{-1}": 1e15, | ||
| r"pm^{-1}": 1e12, | ||
| r"nm^{-1}": 1e9, | ||
| r"um^{-1}": 1e6, | ||
| r"µm^{-1}": 1e6, | ||
| r"mm^{-1}": 1e3, | ||
| r"cm^{-1}": 1e2, | ||
| r"dm^{-1}": 1e1, | ||
| r"m^{-1}": 1, | ||
| r"dam^{-1}": 1e-1, | ||
| r"hm^{-1}": 1e-2, | ||
| r"km^{-1}": 1e-3, | ||
| r"Mm^{-1}": 1e-6, | ||
| r"Gm^{-1}": 1e-9, | ||
| r"Tm^{-1}": 1e-12, | ||
| r"Pm^{-1}": 1e-15, | ||
| r"Em^{-1}": 1e-18, | ||
| r"Zm^{-1}": 1e-21, | ||
| r"Ym^{-1}": 1e-24, | ||
| } | ||
| FREQUENCIES = { | ||
| "yHz": 1e-24, | ||
| "zHz": 1e-21, | ||
| "aHz": 1e-18, | ||
| "fHz": 1e-15, | ||
| "pHz": 1e-12, | ||
| "nHz": 1e-9, | ||
| "uHz": 1e-6, | ||
| "µHz": 1e-6, | ||
| "mHz": 1e-3, | ||
| "cHz": 1e-2, | ||
| "dHz": 1e-1, | ||
| "s": 1, | ||
| "daHz": 1e1, | ||
| "hHz": 1e2, | ||
| "kHz": 1e3, | ||
| "MHz": 1e6, | ||
| "GHz": 1e9, | ||
| "THz": 1e12, | ||
| "PHz": 1e15, | ||
| "EHz": 1e18, | ||
| "ZHz": 1e21, | ||
| "YHz": 1e24, | ||
| r"ys^{-1}": 1e24, | ||
| r"zs^{-1}": 1e21, | ||
| r"as^{-1}": 1e18, | ||
| r"fs^{-1}": 1e15, | ||
| r"ps^{-1}": 1e12, | ||
| r"ns^{-1}": 1e9, | ||
| r"us^{-1}": 1e6, | ||
| r"µs^{-1}": 1e6, | ||
| r"ms^{-1}": 1e3, | ||
| r"cs^{-1}": 1e2, | ||
| r"ds^{-1}": 1e1, | ||
| r"s^{-1}": 1, | ||
| r"das^{-1}": 1e-1, | ||
| r"hs^{-1}": 1e-2, | ||
| r"ks^{-1}": 1e-3, | ||
| r"Ms^{-1}": 1e-6, | ||
| r"Gs^{-1}": 1e-9, | ||
| r"Ts^{-1}": 1e-12, | ||
| r"Ps^{-1}": 1e-15, | ||
| r"Es^{-1}": 1e-18, | ||
| r"Zs^{-1}": 1e-21, | ||
| r"Ys^{-1}": 1e-24, | ||
| } | ||
| def mesh_spheres(radii, positions, model, meshsize=None, meshsize_boundary=None): | ||
| """Generate a mesh of multiple spheres. | ||
| This function facilitates generating a mesh for a cluster spheres using gmsh. It | ||
| requires the package `gmsh` to be installed. | ||
| Examples: | ||
| >>> import gmsh | ||
| >>> import treams.io | ||
| >>> gmsh.initialize() | ||
| >>> gmsh.model.add("spheres") | ||
| >>> treams.io.mesh_spheres([1, 2], [[0, 0, 2], [0, 0, -2]], gmsh.model) | ||
| <class 'gmsh.model'> | ||
| >>> gmsh.write("spheres.msh") # doctest: +SKIP | ||
| >>> gmsh.finalize() # doctest: +SKIP | ||
| Args: | ||
| radii (float, array_like): Radii of the spheres. | ||
| positions (float, (N, 3)-array): Positions of the spheres. | ||
| model (gmsh.model): Gmsh model to modify. | ||
| meshsize (float, optional): Mesh size, if None a fifth of the largest radius is | ||
| used. | ||
| meshsize (float, optional): Mesh size of the surfaces, if left empty it is set | ||
| equal to the general mesh size. | ||
| Returns: | ||
| gmsh.model | ||
| """ | ||
| if meshsize is None: | ||
| meshsize = np.max(radii) * 0.2 | ||
| if meshsize_boundary is None: | ||
| meshsize_boundary = meshsize | ||
| spheres = [] | ||
| for i, (radius, position) in enumerate(zip(radii, positions)): | ||
| tag = i + 1 | ||
| model.occ.addSphere(*position, radius, tag) | ||
| spheres.append((3, tag)) | ||
| model.occ.synchronize() | ||
| for _, tag in spheres: | ||
| model.addPhysicalGroup(3, [tag], tag) | ||
| # Add surfaces for other mesh formats like stl, ... | ||
| model.addPhysicalGroup(2, [tag], tag) | ||
| model.mesh.setSize(model.getEntities(0), meshsize) | ||
| model.mesh.setSize( | ||
| model.getBoundary(spheres, False, False, True), meshsize_boundary | ||
| ) | ||
| return model | ||
| def _translate_polarizations(pols, poltype=None): | ||
| """Translate the polarization index into words. | ||
| The indices 0 and 1 are translated to "negative" and "positive", respectively, when | ||
| helicity modes are chosen. For parity modes they are translated to "magnetic" and | ||
| "electric". | ||
| Args: | ||
| pols (int, array_like): Array of indices 0 and 1. | ||
| poltype (str, optional): Polarization type (:ref:`polarizations.Polarizations`). | ||
| Returns: | ||
| list[str] | ||
| """ | ||
| poltype = treams.config.POLTYPE if poltype is None else poltype | ||
| if poltype == "helicity": | ||
| names = ["negative", "positive"] | ||
| elif poltype == "parity": | ||
| names = ["magnetic", "electric"] | ||
| else: | ||
| raise ValueError("unrecognized poltype") | ||
| return [names[i] for i in pols] | ||
| def _translate_polarizations_inv(pols): | ||
| """Translate the polarization into indices. | ||
| This function is the inverse of :func:`treams.io._translate_polarizations`. The | ||
| words "negative" and "minus" are translated to 0 and the words "positive" and "plus" | ||
| are translated to 1, if helicity modes are chosen. For parity modes, modes | ||
| "magnetic" or "te" are translated to 0 and the modes "electric" or "tm" to 1. | ||
| Args: | ||
| pols (string, array_like): Array of strings | ||
| Returns: | ||
| tuple[list[int], str] | ||
| """ | ||
| helicity = {"plus": 1, "positive": 1, "minus": 0, "negative": 0} | ||
| parity = {"te": 0, "magnetic": 0, "tm": 1, "electric": 1, "M": 0, "N": 1} | ||
| if pols[0].decode() in helicity: | ||
| dct = helicity | ||
| poltype = "helicity" | ||
| elif pols[0].decode() in parity: | ||
| dct = parity | ||
| poltype = "parity" | ||
| else: | ||
| raise ValueError(f"unrecognized polarization '{pols[0].decode()}'") | ||
| return [dct[i.decode()] for i in pols], poltype | ||
| def _remove_leading_ones(shape): | ||
| while len(shape) > 0 and shape[0] == 1: | ||
| shape = shape[1:] | ||
| return shape | ||
| def _collapse_dims(arr): | ||
| for i in range(arr.ndim): | ||
| test = arr[(slice(None),) * i + (slice(1),)] | ||
| if np.all(arr == test): | ||
| arr = test | ||
| return arr.reshape(_remove_leading_ones(arr.shape)) | ||
| def save_hdf5( | ||
| h5file, | ||
| tms, | ||
| name="", | ||
| description="", | ||
| keywords="", | ||
| embedding_group=None, | ||
| embedding_name="", | ||
| embedding_description="", | ||
| embedding_keywords="", | ||
| uuid=None, | ||
| uuid_version=4, | ||
| lunit="nm", | ||
| ): | ||
| """Save a set of T-matrices in a HDF5 file. | ||
| With an open and writeable datafile, this function stores the main parts of as | ||
| T-matrix in the file. It is left open for the user to add additional metadata. | ||
| Args: | ||
| h5file (h5py.Group): A HDF5 file opened with h5py. | ||
| tms (TMatrix, array_like): Array of T-matrix instances. | ||
| name (str): Name to add to the file as attribute. | ||
| description (str): Description to add to file as attribute. | ||
| keywords (str): Keywords to add to file as attribute. | ||
| embedding_group (h5py.Group, optional): Group object for the embedding material, | ||
| defaults to "/materials/embedding/". | ||
| embedding_name (string, optional): Name of the embedding material. | ||
| embedding_description (string, optional): Description of the embedding material. | ||
| embedding_keywords (string, optional): Keywords for the embedding material. | ||
| uuid (bytes, optional): UUID of the file, a new one is created if omitted. | ||
| uuid_version (int, optional): UUID version. | ||
| lunit (string, optional): Length unit used for the positions and (as | ||
| inverse) for the wave number. | ||
| """ | ||
| tms_arr = np.array(tms) | ||
| if tms_arr.dtype == object: | ||
| raise ValueError("can only save T-matrices of the same size") | ||
| tms_obj = np.empty(tms_arr.shape[:-2], object) | ||
| tms_obj[:] = tms | ||
| tm = tms_obj.flat[0] | ||
| basis = tm.basis | ||
| poltype = tm.poltype | ||
| k0s = np.zeros((tms_arr.shape)[:-2]) | ||
| epsilon = np.zeros((tms_arr.shape)[:-2], complex) | ||
| mu = np.zeros((tms_arr.shape)[:-2], complex) | ||
| if poltype == "helicity": | ||
| kappa = np.zeros((tms_arr.shape)[:-2], complex) | ||
| for i, tm in enumerate(tms_obj.flat): | ||
| if poltype != tm.poltype or basis != tm.basis: | ||
| raise ValueError( | ||
| "incompatible T-matrices: mixed poltypes or different bases" | ||
| ) | ||
| k0s.flat[i] = tm.k0 | ||
| epsilon.flat[i] = tm.material.epsilon | ||
| mu.flat[i] = tm.material.mu | ||
| if poltype == "helicity": | ||
| kappa.flat[i] = tm.material.kappa | ||
| h5file["tmatrix"] = tms_arr | ||
| if uuid is None: | ||
| h5file["uuid"] = np.void(_uuid.uuid4().bytes) | ||
| h5file["uuid"].attrs["version"] = 4 | ||
| else: | ||
| h5file["uuid"] = uuid | ||
| h5file["uuid"].attrs["version"] = uuid_version | ||
| _name_descr_kw(h5file, name, description, keywords) | ||
| h5file["angular_vacuum_wavenumber"] = _collapse_dims(k0s) | ||
| h5file["angular_vacuum_wavenumber"].attrs["unit"] = lunit + r"^{-1}" | ||
| h5file["modes/l"] = basis.l | ||
| h5file["modes/m"] = basis.m | ||
| h5file["modes/polarization"] = _translate_polarizations(basis.pol, poltype) | ||
| if any(basis.pidx != 0): | ||
| h5file["modes/pidx"] = basis.pidx | ||
| if not np.array_equiv(basis.positions, [[0, 0, 0]]): | ||
| h5file["modes/positions"] = basis.positions | ||
| h5file["modes/positions"].attrs["unit"] = lunit | ||
| if embedding_group is None: | ||
| embedding_group = h5file.create_group("materials/embedding") | ||
| embedding_group["relative_permittivity"] = _collapse_dims(epsilon) | ||
| embedding_group["relative_permeability"] = _collapse_dims(mu) | ||
| if poltype == "helicity": | ||
| embedding_group["chirality"] = _collapse_dims(kappa) | ||
| _name_descr_kw( | ||
| embedding_group, embedding_name, embedding_description, embedding_keywords | ||
| ) | ||
| h5file["embedding"] = h5py.SoftLink(embedding_group.name) | ||
| h5file.attrs["created_with"] = ( | ||
| f"python={sys.version.split()[0]}," | ||
| f"h5py={version('h5py')}," | ||
| f"treams={version('treams')}" | ||
| ) | ||
| h5file.attrs["storage_format_version"] = "0.0.1-4-g1266244" | ||
| def _name_descr_kw(fobj, name, description="", keywords=""): | ||
| for key, val in [ | ||
| ("name", name), | ||
| ("description", description), | ||
| ("keywords", keywords), | ||
| ]: | ||
| val = str(val) | ||
| if val != "": | ||
| fobj.attrs[key] = val | ||
| def _convert_to_k0(x, xtype, xunit, k0unit=r"nm^{-1}"): | ||
| c = 299792458.0 | ||
| k0unit = INVLENGTHS[k0unit] | ||
| if xtype == "frequency": | ||
| xunit = FREQUENCIES[xunit] | ||
| return 2 * np.pi * x / c * (xunit / k0unit) | ||
| if xtype == "angular_frequency": | ||
| xunit = FREQUENCIES[xunit] | ||
| return x / c * (xunit / k0unit) | ||
| if xtype == "vacuum_wavelength": | ||
| xunit = LENGTHS[xunit] | ||
| return 2 * np.pi / (x * xunit * k0unit) | ||
| if xtype == "vacuum_wavenumber": | ||
| xunit = INVLENGTHS[xunit] | ||
| return 2 * np.pi * x * (xunit / k0unit) | ||
| if xtype == "angular_vacuum_wavenumber": | ||
| xunit = INVLENGTHS[xunit] | ||
| return x * (xunit / k0unit) | ||
| raise ValueError(f"unrecognized frequency/wavenumber/wavelength type: {xtype}") | ||
| def load_hdf5(filename, lunit="nm"): | ||
| """Load a T-matrix stored in a HDF4 file. | ||
| Args: | ||
| filename (str or h5py.Group): Name of the h5py file or a handle to a h5py group. | ||
| lunit (str, optional): Unit of length to be used in the T-matrices. | ||
| Returns: | ||
| np.ndarray[TMatrix] | ||
| """ | ||
| if isinstance(filename, h5py.Group): | ||
| return _load_hdf5(filename, lunit) | ||
| with h5py.File(filename, "r") as f: | ||
| return _load_hdf5(f, lunit) | ||
| def _load_hdf5(h5file, lunit=None): | ||
| for freq_type in ( | ||
| "frequency", | ||
| "angular_frequency", | ||
| "vacuum_wavelength", | ||
| "vacuum_wavenumber", | ||
| "angular_vacuum_wavenumber", | ||
| ): | ||
| if freq_type in h5file: | ||
| ld_freq = h5file[freq_type][()] | ||
| break | ||
| else: | ||
| raise ValueError("no definition of frequency found") | ||
| if "modes/positions" in h5file: | ||
| k0unit = h5file["modes/positions"].attrs.get("unit", lunit) + r"^{-1}" | ||
| else: | ||
| k0unit = lunit + r"^{-1}" | ||
| tms = h5file["tmatrix"][...] | ||
| k0s = _convert_to_k0(ld_freq, freq_type, h5file[freq_type].attrs["unit"], k0unit) | ||
| epsilon = h5file.get("embedding/relative_permittivity", np.array(None))[()] | ||
| mu = h5file.get("embedding/relative_permeability", np.array(None))[()] | ||
| if epsilon is None is mu: | ||
| n = h5file.get("embedding/refractive_index", np.array(1))[...] | ||
| z = h5file.get("embedding/relative_impedance", 1 / n)[...] | ||
| epsilon = n / z | ||
| mu = n * z | ||
| epsilon = 1 if epsilon is None else epsilon | ||
| mu = 1 if mu is None else mu | ||
| kappa = z = h5file.get("embedding/chirality_parameter", np.array(0))[...] | ||
| positions = h5file.get("modes/positions", np.zeros((1, 3)))[...] | ||
| l_inc = h5file.get("modes/l", np.array(None))[...] | ||
| l_inc = h5file.get("modes/l_incident", l_inc)[()] | ||
| m_inc = h5file.get("modes/m", np.array(None))[...] | ||
| m_inc = h5file.get("modes/m_incident", m_inc)[()] | ||
| pol_inc = h5file.get("modes/polarization", np.array(None))[...] | ||
| pol_inc = h5file.get("modes/polarization_incident", pol_inc)[()] | ||
| l_sca = h5file.get("modes/l", np.array(None))[...] | ||
| l_sca = h5file.get("modes/l_scattered", l_sca)[()] | ||
| m_sca = h5file.get("modes/m", np.array(None))[...] | ||
| m_sca = h5file.get("modes/m_scattered", m_sca)[()] | ||
| pol_sca = h5file.get("modes/polarization", np.array(None))[...] | ||
| pol_sca = h5file.get("modes/polarization_scattered", pol_sca)[()] | ||
| if any(x is None for x in (l_inc, l_sca, m_inc, m_sca, pol_inc, pol_sca)): | ||
| raise ValueError("mode definition missing") | ||
| pol_inc, poltype = _translate_polarizations_inv(pol_inc) | ||
| pol_sca, poltype_sca = _translate_polarizations_inv(pol_sca) | ||
| if poltype_sca != poltype: | ||
| raise ValueError("different modetypes") | ||
| pidx_inc = h5file.get("modes/position_index", np.zeros_like(l_inc))[()] | ||
| pidx_inc = h5file.get("modes/positions_index_scattered", pidx_inc)[()] | ||
| pidx_sca = h5file.get("modes/position_index", np.zeros_like(l_sca))[()] | ||
| pidx_sca = h5file.get("modes/positions_index_scattered", pidx_sca)[()] | ||
| shape = tms.shape[:-2] | ||
| k0s = np.broadcast_to(k0s, shape) | ||
| epsilon = np.broadcast_to(epsilon, shape) | ||
| mu = np.broadcast_to(mu, shape) | ||
| kappa = np.broadcast_to(kappa, shape) | ||
| basis_inc = treams.SphericalWaveBasis( | ||
| zip(pidx_inc, l_inc, m_inc, pol_inc), positions | ||
| ) | ||
| basis_sca = treams.SphericalWaveBasis( | ||
| zip(pidx_sca, l_sca, m_sca, pol_sca), positions | ||
| ) | ||
| basis = basis_inc | basis_sca | ||
| ix_inc = [basis.index(b) for b in basis_inc] | ||
| ix_sca = [[basis.index(b)] for b in basis_sca] | ||
| res = np.empty(shape, object) | ||
| for i in np.ndindex(*shape): | ||
| res[i] = treams.TMatrix( | ||
| np.zeros((len(basis),) * 2, complex), | ||
| k0=k0s[i], | ||
| basis=basis, | ||
| poltype=poltype, | ||
| material=treams.Material(epsilon[i], mu[i], kappa[i]), | ||
| ) | ||
| res[i][ix_sca, ix_inc] = tms[i] | ||
| if not res.shape: | ||
| res = res.item() | ||
| return res |
| r"""Lattice sums. | ||
| .. currentmodule:: treams.lattice | ||
| Calculates the lattice sums of the forms | ||
| .. math:: | ||
| D_{lm}(k, \boldsymbol k_\parallel, \boldsymbol r, \Lambda_d) | ||
| = \sideset{}{'}{\sum_{\boldsymbol R \in \Lambda_d}} | ||
| h_l^{(1)}(k|\boldsymbol r + \boldsymbol R|) | ||
| Y_{lm} | ||
| (\theta_{-\boldsymbol r - \boldsymbol R}, \varphi_{-\boldsymbol r - \boldsymbol R}) | ||
| \mathrm e^{\mathrm i \boldsymbol k_\parallel \boldsymbol R} | ||
| and | ||
| .. math:: | ||
| D_{m}(k, \boldsymbol k_\parallel, \boldsymbol r, \Lambda_d) | ||
| = \sideset{}{'}{\sum_{\boldsymbol R \in \Lambda_d}} | ||
| H_m^{(1)}(k|\boldsymbol r + \boldsymbol R|) | ||
| \mathrm e^{\mathrm i m \varphi_{-\boldsymbol r - \boldsymbol R}} | ||
| \mathrm e^{\mathrm i \boldsymbol k_\parallel \boldsymbol R} | ||
| that arise when translating and summing the spherical and cylindrical solutions | ||
| of the Helmholtz equation in a periodic arrangement. These sums have a notoriously slow | ||
| convergence and at least for lattices with a dimension :math:`d > 1` it is not advisable | ||
| to use the direct approach. Fortunately, it is possible to convert them to exponentially | ||
| convergent series, which are implemented here. For details on the math, see the | ||
| references below. | ||
| The lattice of dimension :math:`d \leq 3` (:math:`d \leq 2` in the second case) is | ||
| denoted with :math:`\Lambda_d` and consists of all vectors | ||
| :math:`\boldsymbol R = \sum_{i=1}^{d} n_i \boldsymbol a_i` with :math:`\boldsymbol a_i` | ||
| being basis vectors of the lattice and :math:`n_i \in \mathbb Z`. For :math:`d = 2` the | ||
| lattice is in the `z = 0` plane and for :math:`d = 1` it is along the z-axis. The vector | ||
| :math:`\boldsymbol r` is arbitrary but for best convergence it should be reduced to the | ||
| Wigner-Seitz cell of the lattice. The summation excludes the point | ||
| :math:`\boldsymbol R + \boldsymbol r = 0` if it exists, which is indicated by the prime | ||
| next to the summation sign. | ||
| The wave in the lattice is defined by its -- possibly complex-valued -- wave number | ||
| :math:`k` and the (real) components of the wave vector | ||
| :math:`\boldsymbol k_\parallel \in \mathbb R^d` that are parallel to the lattice. | ||
| The expressions include the (spherical) Hankel functions of first kind :math:`H_m^{(1)}` | ||
| (:math:`h_l^{(1)}`) and the spherical harmonics :math:`Y_{lm}`. The angles | ||
| :math:`\theta` and :math:`\varphi` are the polar and azimuthal angle, when expressing | ||
| the points in spherical coordinates. In the first case, the degree is | ||
| :math:`l \in \mathbb N_0` and the order is :math:`\mathbb Z \ni m \leq l`. In the second | ||
| case :math:`m \in \mathbb Z`. | ||
| Available functions | ||
| =================== | ||
| Accelerated lattice summations | ||
| ------------------------------ | ||
| .. autosummary:: | ||
| :toctree: | ||
| lsumcw1d | ||
| lsumcw1d_shift | ||
| lsumcw2d | ||
| lsumsw1d | ||
| lsumsw1d_shift | ||
| lsumsw2d | ||
| lsumsw2d_shift | ||
| lsumsw3d | ||
| Direct summations | ||
| ----------------- | ||
| The functions are almost only here for benchmarking and comparison. | ||
| .. autosummary:: | ||
| :toctree: | ||
| dsumcw1d | ||
| dsumcw1d_shift | ||
| dsumcw2d | ||
| dsumsw1d | ||
| dsumsw1d_shift | ||
| dsumsw2d | ||
| dsumsw2d_shift | ||
| dsumsw3d | ||
| Miscellaneous functions | ||
| ----------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| area | ||
| cube | ||
| cubeedge | ||
| diffr_orders_circle | ||
| reciprocal | ||
| volume | ||
| Cython module | ||
| ------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| :template: cython-module | ||
| cython_lattice | ||
| References | ||
| ========== | ||
| * `[1] K. Kambe, Zeitschrift Für Naturforschung A 22, 3 (1967). <https://doi.org/10.1515/zna-1967-0305>`_ | ||
| * `[2] K. Kambe, Zeitschrift Für Naturforschung A 23, 9 (1968). <https://doi.org/10.1515/zna-1968-0908>`_ | ||
| * `[3] C. M. Linton, SIAM Rev. 52, 630 (2010). <https://doi.org/10.1137/09075130X>`_ | ||
| * `[4] D. Beutel el al., J. Opt. Soc. Am. B (2021). <https://doi.org/10.1364/JOSAB.419645>`_ | ||
| """ | ||
| import numpy as np | ||
| from treams.lattice import _misc | ||
| from treams.lattice._gufuncs import * # noqa: F403 | ||
| from treams.lattice._misc import cube, cubeedge # noqa: F401 | ||
| def diffr_orders_circle(b, rmax): | ||
| """Diffraction orders in a circle. | ||
| Given the reciprocal lattice defined by the vectors that make up the rows of `b`, | ||
| return all diffraction orders within a circle of radius `rmax`. | ||
| Args: | ||
| b (float, (2, 2)-array): Reciprocal lattice vectors | ||
| rmax (float): Maximal radius | ||
| Returns: | ||
| float array | ||
| """ | ||
| return _misc.diffr_orders_circle(np.array(b), rmax) | ||
| def lsumsw(dim, l, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return lsumsw1d_shift(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return lsumsw2d_shift(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 3: | ||
| return lsumsw3d(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def realsumsw(dim, l, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return realsumsw1d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 2: | ||
| return realsumsw2d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 3: | ||
| return realsumsw3d(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def recsumsw(dim, l, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return recsumsw1d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 2: | ||
| return recsumsw2d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 3: | ||
| return recsumsw3d(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def lsumcw(dim, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return lsumcw1d_shift(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return lsumcw2d(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def realsumcw(dim, m, k, kpar, a, r, eta, out=None, **kwargs): | ||
| if dim == 1: | ||
| return realsumcw1d_shift(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return realsumcw2d(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def recsumcw(dim, m, k, kpar, a, r, eta, out=None, **kwargs): | ||
| if dim == 1: | ||
| return recsumcw1d_shift(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return recsumcw2d(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def dsumsw(dim, l, m, k, kpar, a, r, i, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return dsumsw1d_shift(l, m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return dsumsw2d_shift(l, m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| if dim == 3: | ||
| return dsumsw3d(l, m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def dsumcw(dim, m, k, kpar, a, r, i, out=None, **kwargs): | ||
| if dim == 1: | ||
| return dsumcw1d_shift(m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return dsumcw2d(m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
+230
| """Miscellaneous functions. | ||
| .. autosummary:: | ||
| :toctree: | ||
| basischange | ||
| firstbrillouin1d | ||
| firstbrillouin2d | ||
| firstbrillouin3d | ||
| pickmodes | ||
| refractive_index | ||
| wave_vec_z | ||
| """ | ||
| import numpy as np | ||
| import treams.lattice as la | ||
| def refractive_index(epsilon=1, mu=1, kappa=0): | ||
| r"""Refractive index of a (chiral) medium. | ||
| The refractive indeces in a chiral medium :math:`\sqrt{\epsilon\mu} \mp \kappa` are | ||
| returned with the negative helicity result first. | ||
| Args: | ||
| epsilon (float or complex, array_like, optional): Relative permittivity, | ||
| defaults to 1. | ||
| mu (float or complex, array_like, optional): Relative permeability, | ||
| defaults to 1. | ||
| kappa (float or complex, array_like, optional): Chirality parameter, | ||
| defaults to 0. | ||
| Returns: | ||
| float or complex, (2,)-array | ||
| """ | ||
| epsilon = np.array(epsilon) | ||
| n = np.sqrt(epsilon * mu) | ||
| res = np.stack((n - kappa, n + kappa), axis=-1) | ||
| res[np.imag(res) < 0] *= -1 | ||
| return res | ||
| def basischange(out, in_=None): | ||
| """Coefficients for the basis change between helicity and parity modes. | ||
| Args: | ||
| out (3- or 4-tuple of (M,)-arrays): Output modes, the last array is taken as | ||
| polarization. | ||
| in_ (3- or 4-tuple of (N,)-arrays, optional): Input modes, if none are given, | ||
| equal to the output modes | ||
| Returns: | ||
| float, ((M, N)-array | ||
| """ | ||
| if in_ is None: | ||
| in_ = out | ||
| out = np.array([*zip(*out)]) | ||
| in_ = np.array([*zip(*in_)]) | ||
| res = np.zeros((out.shape[0], in_.shape[0])) | ||
| out = out[:, None, :] | ||
| sqhalf = np.sqrt(0.5) | ||
| equal = (out[:, :, :-1] == in_[:, :-1]).all(axis=-1) | ||
| minus = np.logical_and(out[:, :, -1] == in_[:, -1], in_[:, -1] == 0) | ||
| res[equal] = sqhalf | ||
| res[np.logical_and(equal, minus)] = -sqhalf | ||
| return res | ||
| def pickmodes(out, in_): | ||
| """Coefficients to pick modes. | ||
| Args: | ||
| out (3- or 4-tuple of (M,)-arrays): Output modes, the last array is taken as | ||
| polarization. | ||
| in_ (3- or 4-tuple of (N,)-arrays, optional): Input modes, if none are given, | ||
| equal to the output modes | ||
| Returns: | ||
| float, ((M, N)-array | ||
| """ | ||
| out = np.array([*zip(*out)]) | ||
| in_ = np.array([*zip(*in_)]) | ||
| return np.all(out[:, None, :] == in_, axis=-1) | ||
| # def wave_vec_zs(kpars, ks): | ||
| # kpars = np.array(kpars) | ||
| # ks = np.array(ks) | ||
| # if ks.ndim == 0: | ||
| # nmat = npol = 1 | ||
| # elif ks.ndim == 1: | ||
| # npol = ks.shape[0] | ||
| # nmat = 1 | ||
| # elif ks.ndim == 2: | ||
| # nmat, npol = ks.shape | ||
| # else: | ||
| # raise ValueError("ks has invalid shape") | ||
| # res = np.zeros((nmat, kpars.shape[0], npol), np.complex128) | ||
| # for i, kpar in enumerate(kpars): | ||
| # res[:, i, :] = wave_vec_z(kpar[0], kpar[1], ks) | ||
| # return res | ||
| def wave_vec_z(kx, ky, k): | ||
| r"""Z component of the wave vector with positive imaginary part. | ||
| The result is :math:`k_z = \sqrt{k^2 - k_x^2 - k_y^2}` with | ||
| :math:`\arg k_z \in \[ 0, \pi )`. | ||
| Args: | ||
| kx (float, array_like): X component of the wave vector | ||
| ky (float, array_like): Y component of the wave vector | ||
| k (float or complex, array_like): Wave number | ||
| Returns: | ||
| complex | ||
| """ | ||
| kx = np.asarray(kx) | ||
| ky = np.asarray(ky) | ||
| k = np.asarray(k, complex) | ||
| res = np.sqrt(k * k - kx * kx - ky * ky) | ||
| if res.ndim == 0 and res.imag < 0: | ||
| res = -res | ||
| elif res.ndim > 0: | ||
| res[np.imag(res) < 0] *= -1 | ||
| return res | ||
| def firstbrillouin1d(kpar, b): | ||
| """Map wave vector to first Brillouin zone in 1D. | ||
| Reduce the 1d wave vector (actually just a number) to the first Brillouin zone, i.e. | ||
| the range `(-b/2, b/2]` | ||
| Args: | ||
| kpar (float64): (parallel) wave vector | ||
| b (float64): reciprocal lattice vector | ||
| Returns: | ||
| float64 | ||
| """ | ||
| kpar -= b * np.round(kpar / b) | ||
| if kpar > 0.5 * b: | ||
| kpar -= b | ||
| if kpar <= -0.5 * b: | ||
| kpar += b | ||
| return kpar | ||
| def firstbrillouin2d(kpar, b, n=2): | ||
| """Map wave vector to first Brillouin zone in 2D. | ||
| The reduction to the first Brillouin zone is first approximated roughly. From this | ||
| approximated vector and its 8 neighbours, the shortest one is picked. As a | ||
| sufficient approximation is not guaranteed (especially for extreme geometries), | ||
| this process is iterated `n` times. | ||
| Args: | ||
| kpar (1d-array): parallel wave vector | ||
| b (2d-array): reciprocal lattice vectors | ||
| n (int): number of iterations | ||
| Returns: | ||
| (1d-array) | ||
| """ | ||
| kparstart = kpar | ||
| b1 = b[0, :] | ||
| b2 = b[1, :] | ||
| normsq1 = b1 @ b1 | ||
| normsq2 = b2 @ b2 | ||
| normsqp = (b1 + b2) @ (b1 + b2) | ||
| normsqm = (b1 - b2) @ (b1 - b2) | ||
| if ( | ||
| normsqp < normsq1 - 1e-14 | ||
| or normsqp < normsq2 - 1e-14 | ||
| or normsqm < normsq1 - 1e-14 | ||
| or normsqm < normsq2 - 1e-14 | ||
| ): | ||
| raise ValueError("Lattice vectors are not of minimal length") | ||
| # Rough estimate | ||
| kpar -= b1 * np.round((kpar @ b1) / normsq1) | ||
| kpar -= b2 * np.round((kpar @ b2) / normsq2) | ||
| # todo: precise | ||
| options = kpar + la.cube(2, 1) @ b | ||
| for option in options: | ||
| if option @ option < kpar @ kpar: | ||
| kpar = option | ||
| if n == 0 or np.array_equal(kpar, kparstart): | ||
| return kpar | ||
| return firstbrillouin2d(kpar, b, n - 1) | ||
| def firstbrillouin3d(kpar, b, n=2): | ||
| """Map wave vector to first Brillouin zone in 3D. | ||
| The reduction to the first Brillouin zone is first approximated roughly. From this | ||
| approximated vector and its 26 neighbours, the shortest one is picked. As a | ||
| sufficient approximation is not guaranteed (especially for extreme geometries), | ||
| this process is iterated `n` times. | ||
| Args: | ||
| kpar (1d-array): parallel wave vector | ||
| b (2d-array): reciprocal lattice vectors | ||
| n (int): number of iterations | ||
| Returns: | ||
| (1d-array) | ||
| """ | ||
| kparstart = kpar | ||
| b1 = b[0, :] | ||
| b2 = b[1, :] | ||
| b3 = b[2, :] | ||
| normsq1 = b1 @ b1 | ||
| normsq2 = b2 @ b2 | ||
| normsq3 = b3 @ b3 | ||
| # todo: Minimal length | ||
| # Rough estimate | ||
| kpar -= b1 * np.round((kpar @ b1) / normsq1) | ||
| kpar -= b2 * np.round((kpar @ b2) / normsq2) | ||
| kpar -= b3 * np.round((kpar @ b3) / normsq3) | ||
| # todo: precise | ||
| options = kpar + la.cube(3, 1) @ b | ||
| for option in options: | ||
| if option @ option < kpar @ kpar: | ||
| kpar = option | ||
| if n == 0 or np.array_equal(kpar, kparstart): | ||
| return kpar | ||
| return firstbrillouin3d(kpar, b, n - 1) |
Sorry, the diff of this file is too big to display
| r"""Special (mathematical) functions. | ||
| .. currentmodule:: treams.special | ||
| Special mathematical functions used in :mod:`treams`. Some functions are reexported from | ||
| :py:mod:`scipy.special`. Most functions are available as Numpy universal functions | ||
| (:py:class:`numpy.ufunc`) or as generalized universal functions | ||
| (:ref:`c-api.generalized-ufuncs`). | ||
| Available functions | ||
| =================== | ||
| Bessel and Hankel functions, with their spherical counterparts, derivatives | ||
| --------------------------------------------------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| hankel1_d | ||
| hankel2_d | ||
| jv_d | ||
| yv_d | ||
| spherical_hankel1 | ||
| spherical_hankel2 | ||
| spherical_hankel1_d | ||
| spherical_hankel2_d | ||
| Those functions are just reexported from Scipy. So, one only needs to import this | ||
| subpackage within treams. | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.hankel1`\(v, z[, out]) | Hankel function of the | | ||
| | | first kind. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.hankel2`\(v, z[, out]) | Hankel function of the | | ||
| | | second kind. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.jv`\(v, z[, out]) | Bessel function of the | | ||
| | | first kind of real | | ||
| | | order and complex | | ||
| | | argument. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.yv`\(v, z[, out]) | Bessel function of the | | ||
| | | second kind of real | | ||
| | | order and complex | | ||
| | | argument. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | | :py:func:`spherical_jn <scipy.special.spherical_jn>`\(n, | Spherical Bessel | | ||
| | z[, derivative]) | function of the first | | ||
| | | kind or its derivative. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | | :py:func:`spherical_yn <scipy.special.spherical_yn>`\(n, | Spherical Bessel | | ||
| | z[, derivative]) | function of the second | | ||
| | | kind or its derivative. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| Those functions just wrap Scipy functions with special optional arguments to be able to | ||
| analogously access them like their non-spherical counterparts: | ||
| .. autosummary:: | ||
| :toctree: | ||
| spherical_jn_d | ||
| spherical_yn_d | ||
| Scipy functions with enhanced domain | ||
| ------------------------------------ | ||
| .. autosummary:: | ||
| :toctree: | ||
| sph_harm | ||
| lpmv | ||
| Integrals for the Ewald summation | ||
| --------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| incgamma | ||
| intkambe | ||
| Wigner d- and Wigner D-matrix elements | ||
| -------------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| wignersmalld | ||
| wignerd | ||
| Wigner 3j-symbols | ||
| ----------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| wigner3j | ||
| Vector wave functions | ||
| -------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| pi_fun | ||
| tau_fun | ||
| Spherical waves and translation coefficients | ||
| .. autosummary:: | ||
| :toctree: | ||
| vsh_X | ||
| vsh_Y | ||
| vsh_Z | ||
| vsw_M | ||
| vsw_N | ||
| vsw_A | ||
| vsw_rM | ||
| vsw_rN | ||
| vsw_rA | ||
| tl_vsw_A | ||
| tl_vsw_B | ||
| tl_vsw_rA | ||
| tl_vsw_rB | ||
| Cylindrical waves | ||
| .. autosummary:: | ||
| :toctree: | ||
| vcw_M | ||
| vcw_N | ||
| vcw_A | ||
| vcw_rM | ||
| vcw_rN | ||
| vcw_rA | ||
| tl_vcw | ||
| tl_vcw_r | ||
| Plane waves | ||
| .. autosummary:: | ||
| :toctree: | ||
| vpw_M | ||
| vpw_N | ||
| vpw_A | ||
| Coordinate system transformations | ||
| --------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| car2cyl | ||
| car2sph | ||
| cyl2car | ||
| cyl2sph | ||
| sph2car | ||
| sph2cyl | ||
| vcar2cyl | ||
| vcar2sph | ||
| vcyl2car | ||
| vcyl2sph | ||
| vsph2car | ||
| vsph2cyl | ||
| car2pol | ||
| pol2car | ||
| vcar2pol | ||
| vpol2car | ||
| Cython module | ||
| ------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| :template: cython-module | ||
| cython_special | ||
| """ | ||
| from scipy.special import ( # noqa: F401 | ||
| hankel1, | ||
| hankel2, | ||
| jv, | ||
| spherical_jn, | ||
| spherical_yn, | ||
| yv, | ||
| ) | ||
| from treams.special import _gufuncs, _ufuncs # noqa: F401 | ||
| from treams.special._gufuncs import * # noqa: F401, F403 | ||
| from treams.special._ufuncs import * # noqa: F401, F403 | ||
| def spherical_jn_d(n, z): | ||
| """Derivative of the spherical Bessel function of the first kind. | ||
| This is simply a wrapper for `scipy.special.spherical_jn(n, z, True)`, see | ||
| :py:func:`scipy.special.spherical_jn`. It's here to have a consistent way of | ||
| calling the derivative of a (spherical) Bessel or Hankel function. | ||
| Args: | ||
| n (int, array_like): Order | ||
| z (float or complex, array_like): Argument | ||
| Returns: | ||
| float or complex | ||
| References: | ||
| - `DLMF 10.47 <https://dlmf.nist.gov/10.47>`_ | ||
| - `DLMF 10.51 <https://dlmf.nist.gov/10.51>`_ | ||
| """ | ||
| return spherical_jn(n, z, True) | ||
| def spherical_yn_d(n, z): | ||
| """Derivative of the spherical Bessel function of the second kind. | ||
| This is simply a wrapper for `scipy.special.spherical_yn(n, z, True)`, see | ||
| :py:func:`scipy.special.spherical_jn`. It's here to have a consistent way of | ||
| calling the derivative of a (spherical) Bessel or Hankel function. | ||
| Args: | ||
| n (int, array_like): Order | ||
| z (float or complex, array_like): Argument | ||
| Returns: | ||
| float or complex | ||
| References: | ||
| - `DLMF 10.47 <https://dlmf.nist.gov/10.47>`_ | ||
| - `DLMF 10.51 <https://dlmf.nist.gov/10.51>`_ | ||
| """ | ||
| return spherical_yn(n, z, True) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
+1223
| """Utilities. | ||
| Collection of functions to make :class:`AnnotatedArray` work. The implementation is | ||
| aimed to be quite general, while still providing a solid basis for the rest of the code. | ||
| """ | ||
| import abc | ||
| import collections | ||
| import copy | ||
| import itertools | ||
| import warnings | ||
| import numpy as np | ||
| import treams._operators as _op | ||
| __all__ = [ | ||
| "AnnotatedArray", | ||
| "AnnotationDict", | ||
| "AnnotationError", | ||
| "AnnotationSequence", | ||
| "AnnotationWarning", | ||
| "implements", | ||
| "OrderedSet", | ||
| ] | ||
| class OrderedSet(collections.abc.Sequence, collections.abc.Set): | ||
| """Ordered set. | ||
| A abstract base class that combines a sequence and set. In contrast to regular sets | ||
| it is expected that the equality comparison only returns `True` if all entries are | ||
| in the same order. | ||
| """ | ||
| @abc.abstractmethod | ||
| def __eq__(self, other): | ||
| """Equality test.""" | ||
| raise NotImplementedError | ||
| class AnnotationWarning(UserWarning): | ||
| """Custom warning for Annotations. | ||
| By default the warning filter is set to 'always'. | ||
| """ | ||
| warnings.simplefilter("always", AnnotationWarning) | ||
| class AnnotationError(Exception): | ||
| """Custom exception for Annotations.""" | ||
| class AnnotationDict(collections.abc.MutableMapping): | ||
| """Dictionary that notifies the user when overwriting keys. | ||
| Behaves mostly similar to regular dictionaries, except that when overwriting | ||
| existing keys a :class:`AnnotationWarning` is emmitted. | ||
| Examples: | ||
| An :class:`AnnotationDict` can be created from other mappings, from a list of | ||
| key-value pairs (note how a warning is emmitted for the duplicate key), and from | ||
| keyword arguments. | ||
| .. code-block:: python | ||
| >>> AnnotationDict({"a": 1, "b": 2}) | ||
| AnnotationDict({'a': 1, 'b': 2}) | ||
| >>> AnnotationDict([("a", 1), ("b", 2), ("a", 3)]) | ||
| treams/util.py:74: AnnotationWarning: overwriting key 'a' | ||
| warnings.warn(f"overwriting key '{key}'", AnnotationWarning) | ||
| AnnotationDict({'a': 3, 'b': 2}) | ||
| >>> AnnotationDict({"a": 1, "b": 2}, c=3) | ||
| AnnotationDict({'a': 1, 'b': 2, 'c': 3}) | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| def __init__(self, items=(), /, **kwargs): | ||
| """Initialization.""" | ||
| self._dct = {} | ||
| for i in (items.items() if hasattr(items, "items") else items, kwargs.items()): | ||
| for key, val in i: | ||
| self[key] = val | ||
| def __getitem__(self, key): | ||
| """Get a value by its key. | ||
| Args: | ||
| key (hashable): Key | ||
| """ | ||
| return self._dct[key] | ||
| def __setitem__(self, key, val): | ||
| """Set item specified by key to the defined value. | ||
| When overwriting an existing key an :class:`AnnotationWarning` is emitted. | ||
| Avoid the warning by explicitly deleting the key first. | ||
| Args: | ||
| key (hashable): Key | ||
| val : Value | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| if key in self and self[key] != val: | ||
| warnings.warn(f"overwriting key '{key}'", AnnotationWarning) | ||
| self._dct[key] = val | ||
| def __delitem__(self, key): | ||
| """Delete the key.""" | ||
| del self._dct[key] | ||
| def __iter__(self): | ||
| """Iterate over the keys. | ||
| Returns: | ||
| Iterator | ||
| """ | ||
| return iter(self._dct) | ||
| def __len__(self): | ||
| """Number of keys contained. | ||
| Returns: | ||
| int | ||
| """ | ||
| return len(self._dct) | ||
| def __repr__(self): | ||
| """String representation. | ||
| Returns: | ||
| str | ||
| """ | ||
| return f"{self.__class__.__name__}({repr(self._dct)})" | ||
| def match(self, other): | ||
| """Compare the own keys to another dictionary. | ||
| This emits an :class:`AnnotationWarning` for each key that would be overwritten | ||
| by the given dictionary. | ||
| Args: | ||
| other (Mapping) | ||
| Returns: | ||
| None | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| for key, val in self.items(): | ||
| if key in other and other[key] != val: | ||
| warnings.warn(f"incompatible key '{key}'", AnnotationWarning) | ||
| class SequenceAsDict(collections.abc.MutableMapping): | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| return self | ||
| def __set__(self, obj, dct): | ||
| self._obj = obj | ||
| for key, val in dct.items(): | ||
| self[key] = val | ||
| def __getitem__(self, key): | ||
| res = tuple(i.get(key) for i in self._obj) | ||
| if all(i is None for i in res): | ||
| raise KeyError(key) | ||
| return res | ||
| def __setitem__(self, key, val): | ||
| val = (None,) * (len(self._obj) - len(val)) + val | ||
| for dct, value in zip(reversed(self._obj), reversed(val)): | ||
| if value is None: | ||
| dct.pop(key, None) | ||
| else: | ||
| dct[key] = value | ||
| def __delitem__(self, key): | ||
| found = False | ||
| for dct in self._obj: | ||
| if key in dct: | ||
| del dct[key] | ||
| found = True | ||
| if not found: | ||
| raise KeyError(key) | ||
| def __iter__(self): | ||
| return iter({key for dct in self._obj for key in dct}) | ||
| def __len__(self): | ||
| return len({key for dct in self._obj for key in dct}) | ||
| def __repr__(self): | ||
| return f"{self.__class__.__name__}({dict(i for i in self.items())})" | ||
| class AnnotationSequence(collections.abc.Sequence): | ||
| """A Sequence of dictionaries. | ||
| This class is intended to work together with :class:`AnnotationDict`. It provides | ||
| convenience functions to interact with multiple of those dictionaries, which are | ||
| mainly used to keep track of the annotations made to each dimension of an | ||
| :class:`AnnotatedArray`. While the sequence itself is immutable the entries of each | ||
| dictionary is mutable. | ||
| Args: | ||
| *args: Items of the sequence | ||
| mapping (AnnotationDict): Type of mapping to use in the sequence. | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| as_dict = SequenceAsDict() | ||
| def __init__(self, *args, mapping=AnnotationDict): | ||
| """Initialization.""" | ||
| self._ann = tuple(mapping(i) for i in args) | ||
| def __len__(self): | ||
| """Number of dictionaries in the sequence. | ||
| Returns: | ||
| int | ||
| """ | ||
| return len(self._ann) | ||
| def __getitem__(self, key): | ||
| """Get an item or subsequence. | ||
| Indexing works with integers and slices like regular tuples. Additionally, it is | ||
| possible to get a copy of the object with `()`, or a new sequence of mappings in | ||
| a list (or other iterable) of integers. | ||
| Args: | ||
| key (iterable, slice, int) | ||
| Returns: | ||
| mapping | ||
| """ | ||
| if isinstance(key, tuple) and key == (): | ||
| return copy.copy(self._ann) | ||
| if isinstance(key, slice): | ||
| return type(self)(*self._ann[key]) | ||
| if isinstance(key, (int, np.integer)): | ||
| return self._ann[key] | ||
| res = [] | ||
| for k in key: | ||
| if not isinstance(k, int): | ||
| raise TypeError( | ||
| "sequence index must be integer, slice, list of integers, or '()'" | ||
| ) | ||
| res.append(self[k]) | ||
| return type(self)(*res) | ||
| def update(self, other): | ||
| """Update all mappings in the sequence at once. | ||
| The given sequence is aliged at the last entry and then updated pairwise. | ||
| Warnings for overwritten keys are extended by the information at which index | ||
| they occurred. | ||
| Args: | ||
| other (Sequence[Mapping]): Mappings to update with. | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| if len(other) > len(self): | ||
| warnings.warn( | ||
| f"argument of length {len(self)} given: " | ||
| f"ignore leading {len(other) - len(self)} entries", | ||
| AnnotationWarning, | ||
| ) | ||
| for i, (dest, src) in enumerate(zip(reversed(self), reversed(other))): | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error", category=AnnotationWarning) | ||
| try: | ||
| dest.update(src) | ||
| except AnnotationWarning as err: | ||
| warnings.simplefilter("always", category=AnnotationWarning) | ||
| warnings.warn( | ||
| f"at index {len(self) - i - 1}: " + err.args[0], | ||
| AnnotationWarning, | ||
| ) | ||
| def match(self, other): | ||
| """Match all mappings at once. | ||
| The given sequence is aliged at the last entry and then updated pairwise. | ||
| Warnings for overwritten keys are extended by the information at which index | ||
| they occurred, see also :func:`AnnotationDict.match`. | ||
| Args: | ||
| other (Sequence[Mappings]): Mappings to match. | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| for i, (dest, src) in enumerate(zip(reversed(self), reversed(other))): | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error", category=AnnotationWarning) | ||
| try: | ||
| dest.match(src) | ||
| except AnnotationWarning as err: | ||
| warnings.simplefilter("always", category=AnnotationWarning) | ||
| warnings.warn( | ||
| f"at dimension {len(self) + i}: " + err.args[0], | ||
| AnnotationWarning, | ||
| ) | ||
| def __eq__(self, other): | ||
| """Equality test. | ||
| Two sequences of mappings are considered equal when they have equal length and | ||
| when they all mappings are equal. | ||
| Args: | ||
| other (Sequence[Mapping]): Mappings to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| try: | ||
| lenother = len(other) | ||
| except TypeError: | ||
| return False | ||
| if len(self) != lenother: | ||
| return False | ||
| for a, b in zip(self, other): | ||
| if a != b: | ||
| return False | ||
| return True | ||
| def __repr__(self): | ||
| """String representation. | ||
| Returns: | ||
| str | ||
| """ | ||
| if len(self) == 1: | ||
| return f"{type(self).__name__}({repr(self._ann[0])})" | ||
| return f"{type(self).__name__}{repr(self._ann)}" | ||
| def __add__(self, other): | ||
| return AnnotationSequence(*self, *other) | ||
| def __radd__(self, other): | ||
| return AnnotationSequence(*other, *self) | ||
| def _cast_nparray(arr): | ||
| """Cast AnnotatedArray to numpy array.""" | ||
| return np.asarray(arr) if isinstance(arr, AnnotatedArray) else arr | ||
| def _cast_annarray(arr): | ||
| """Cast array to AnnotatedArray.""" | ||
| return arr if isinstance(arr, np.generic) else AnnotatedArray(arr) | ||
| def _parse_signature(signature, inputdims): | ||
| """Parse a ufunc signature based on the actual inputs. | ||
| The signature is matched with the input dimensions, to get the actual signature. It | ||
| is returned as two lists (inputs and outputs). Each list contains for each | ||
| individual input and output another list of the signature items. | ||
| Example: | ||
| >>> treams.util._parse_signature('(n?,k),(k,m?)->(n?,m?)', (2, 1)) | ||
| ([['n?', 'k'], ['k']], [['n?']]) | ||
| Args: | ||
| signature (str): Function signature | ||
| inputdims (Iterable[int]): Input dimensions | ||
| Returns: | ||
| tuple[list[list[str]] | ||
| """ | ||
| signature = "".join(signature.split()) # remove whitespace | ||
| sigin, sigout = signature.split("->") # split input and output | ||
| sigin = sigin[1:-1].split("),(") # split input | ||
| sigin = [i.split(",") for i in sigin] | ||
| sigout = sigout[1:-1].split("),(") # split output | ||
| sigout = [i.split(",") for i in sigout] | ||
| for i, idim in enumerate(inputdims): | ||
| j = 0 | ||
| while j < len(sigin[i]): | ||
| d = sigin[i][j] | ||
| if d.endswith("?") and len(sigin[i]) > idim: | ||
| sigin = [[i for i in s if i != d] for s in sigin] | ||
| sigout = [[i for i in s if i != d] for s in sigout] | ||
| else: | ||
| j += 1 | ||
| return sigin, sigout | ||
| def _parse_key(key, ndim): | ||
| """Parse a key to index an array. | ||
| This function attempts to replicate the numpy indexing of arrays. It can handle | ||
| integers, slices, an Ellipsis, arrays of integers, arrays of bools, (bare) bools, | ||
| and None. It returns the key with an Ellipsis appended if the number of index | ||
| dimensions does not match the number of dimensions given. Additionally, it informs | ||
| about the number of dimension indexed by fancy indexing, if the fancy indexed | ||
| dimensions will be prepended, and the number of dimensions that the Ellipsis | ||
| contains. | ||
| Args: | ||
| key (tuple): The indexing key | ||
| ndim (int): Number of array dimensions | ||
| Returns: | ||
| tuple | ||
| """ | ||
| consumed = 0 | ||
| ellipsis = False | ||
| fancy_ndim = 0 | ||
| # nfancy = 0 | ||
| consecutive_intfancy = 0 | ||
| # consecutive_intfancy = 0: no fancy/integer indexing | ||
| # consecutive_intfancy = 1: ongoing consecutive fancy/integer indexing | ||
| # consecutive_intfancy = 2: terminated consecutive fancy/integer indexing | ||
| # consecutive_intfancy >= 2: non-consecutive fancy/integer indexing | ||
| # The first pass gets the consumed dimensions, the presence of an ellipsis, and | ||
| # fancy index properties. | ||
| for k in key: | ||
| if k is not True and k is not False and isinstance(k, (int, np.integer)): | ||
| consumed += 1 | ||
| consecutive_intfancy += (consecutive_intfancy + 1) % 2 | ||
| elif k is None: | ||
| consecutive_intfancy += consecutive_intfancy % 2 | ||
| elif isinstance(k, slice): | ||
| consumed += 1 | ||
| consecutive_intfancy += consecutive_intfancy % 2 | ||
| elif k is Ellipsis: | ||
| ellipsis = True | ||
| # consumed is determined at the end | ||
| consecutive_intfancy += consecutive_intfancy % 2 | ||
| else: | ||
| arr = np.asanyarray(k) | ||
| if arr.dtype == bool: | ||
| consumed += arr.ndim | ||
| fancy_ndim = max(1, fancy_ndim) | ||
| else: | ||
| consumed += 1 | ||
| fancy_ndim = max(arr.ndim, fancy_ndim) | ||
| # nfancy += arr.ndim | ||
| consecutive_intfancy += (consecutive_intfancy + 1) % 2 | ||
| lenellipsis = ndim - consumed | ||
| if lenellipsis != 0 and not ellipsis: | ||
| key = key + (Ellipsis,) | ||
| return key, fancy_ndim, consecutive_intfancy >= 2, lenellipsis | ||
| HANDLED_FUNCTIONS = {} | ||
| """Dictionary of numpy functions implemented for AnnotatedArrays.""" | ||
| def implements(np_func): | ||
| """Decorator to register an __array_function__ implementation to AnnotatedArrays.""" | ||
| def decorator(func): | ||
| HANDLED_FUNCTIONS[np_func] = func | ||
| return func | ||
| return decorator | ||
| class AnnotatedArray(np.lib.mixins.NDArrayOperatorsMixin): | ||
| """Array that keeps track of annotations for each dimension. | ||
| This class acts mostly like numpy arrays, but it is enhanced by the following | ||
| functionalities: | ||
| * Annotations are added to each dimension | ||
| * Annotations are compared and preserved for numpy (generalized) | ||
| :py:class:`numpy.ufunc` (like :py:data:`numpy.add`, :py:data:`numpy.exp`, | ||
| :py:data:`numpy.matmul` and many more "standard" mathematical functions) | ||
| * Special ufunc methods, like :py:meth:`numpy.ufunc.reduce`, are supported | ||
| * A growing subset of other numpy functions are supported, like | ||
| :py:func:`numpy.linalg.solve` | ||
| * Keywords can be specified as scale are also index into, when index the | ||
| AnnotatedArray | ||
| * Annotations can also be exposed as properties | ||
| .. testsetup:: | ||
| from treams.util import AnnotatedArray | ||
| Example: | ||
| >>> a = AnnotatedArray([[0, 1], [2, 3]], ({"a": 1}, {"b": 2})) | ||
| >>> b = AnnotatedArray([1, 2], ({"b": 2},)) | ||
| >>> a @ b | ||
| AnnotatedArray( | ||
| [2, 8], | ||
| AnnotationSequence(AnnotationDict({'a': 1})), | ||
| ) | ||
| The interoperability with numpy is implemented using :ref:`basics.dispatch` | ||
| by defining :meth:`__array__`, :meth:`__array_ufunc__`, and | ||
| :meth:`__array_function__`. | ||
| """ | ||
| _scales = set() | ||
| def __init__(self, array, ann=(), /, **kwargs): | ||
| """Initalization.""" | ||
| self._array = np.asarray(array) | ||
| self.ann = getattr(array, "ann", ()) | ||
| self.ann.update(ann) | ||
| for key, val in kwargs.items(): | ||
| val = (val,) * self.ndim if not isinstance(val, tuple) else val | ||
| self.ann.as_dict[key] = val | ||
| @classmethod | ||
| def relax(cls, *args, mro=None, **kwargs): | ||
| """Try creating AnnotatedArray subclasses if possible. | ||
| Subclasses can impose stricter conditions on the Annotations. To allow a simple | ||
| "decaying" of those subclasses it is possible to create them with this | ||
| classmethod. It attempts array creations along the method resolution order until | ||
| it succeeds. | ||
| Args: | ||
| mro (array-like, optional): Method resolution order along which to create | ||
| the subclass. By default it takes the order of the calling class. | ||
| Note: | ||
| All other arguments are the same as for the default initialization. | ||
| """ | ||
| mro = cls.__mro__[1:] if mro is None else mro | ||
| try: | ||
| return cls(*args, **kwargs) | ||
| except AnnotationError as err: | ||
| if cls == AnnotatedArray: | ||
| raise err from None | ||
| cls, *mro = mro | ||
| return cls.relax(*args, mro=mro, **kwargs) | ||
| def __str__(self): | ||
| """String of the array itself.""" | ||
| return str(self._array) | ||
| def __repr__(self): | ||
| """String representation.""" | ||
| repr_arr = " " + repr(self._array)[6:-1].replace("\n ", "\n") | ||
| return f"{self.__class__.__name__}(\n{repr_arr},\n {self.ann},\n)" | ||
| def __int__(self): | ||
| return int(self._array) | ||
| def __float__(self): | ||
| return float(self._array) | ||
| def __complex__(self): | ||
| return complex(self._array) | ||
| def __array__(self, dtype=None): | ||
| """Convert to an numpy array. | ||
| This function returns the bare array without annotations. This function does not | ||
| necessarily make a copy of the array. | ||
| Args: | ||
| dtype (optional): Type of the returned array. | ||
| """ | ||
| return np.asarray(self._array, dtype=dtype) | ||
| @property | ||
| def ann(self): | ||
| """Array annotations.""" | ||
| return self._ann | ||
| @ann.setter | ||
| def ann(self, ann): | ||
| """Set array annotations. | ||
| This function copies the given sequence of dictionaries. | ||
| """ | ||
| self._ann = AnnotationSequence(*(({},) * self.ndim)) | ||
| self._ann.update(ann) | ||
| def __getattr__(self, key): | ||
| # In most cases, we shouldn't arrive here with the key "_ann", an exception is | ||
| # pickle.load which needs this early error to not result in an infinite | ||
| # recursion | ||
| if key == "_ann": | ||
| raise AttributeError() | ||
| if key in self._ann.as_dict: | ||
| res = self._ann.as_dict[key] | ||
| if all(res[0] == i for i in res[1:]): | ||
| return res[0] | ||
| return res | ||
| raise AttributeError( | ||
| f"'{self.__class__.__name__}' object has no attribute '{key}'" | ||
| ) | ||
| def __setattr__(self, key, val): | ||
| if key not in ("_array", "ann", "_ann") and key in self.ann.as_dict: | ||
| val = (val,) * self.ndim if not isinstance(val, tuple) else val | ||
| self.ann.as_dict[key] = val | ||
| else: | ||
| super().__setattr__(key, val) | ||
| def __delattr__(self, key): | ||
| try: | ||
| super().__delattr__(key) | ||
| except AttributeError: | ||
| del self.ann.as_dict[key] | ||
| def __len__(self): | ||
| return len(self._array) | ||
| def __copy__(self): | ||
| return type(self)(copy.copy(self._array), copy.copy(self._ann)) | ||
| def __deepcopy__(self, memo): | ||
| cls = type(self) | ||
| return cls(copy.deepcopy(self._array, memo), copy.deepcopy(self._ann, memo)) | ||
| def __bool__(self): | ||
| """Boolean value of the array.""" | ||
| return bool(self._array) | ||
| def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): | ||
| """Implement ufunc API.""" | ||
| # Compute result first to use numpy's comprehensive checks on the arguments | ||
| inputs_noaa = tuple(map(_cast_nparray, inputs)) | ||
| ann_out = () | ||
| out = kwargs.get("out") | ||
| out = out if isinstance(out, tuple) else (out,) | ||
| ann_out = tuple(getattr(i, "ann", None) for i in out) | ||
| if len(out) != 1 or out[0] is not None: | ||
| kwargs["out"] = tuple(map(_cast_nparray, out)) | ||
| res = getattr(ufunc, method)(*inputs_noaa, **kwargs) | ||
| istuple, res = (True, res) if isinstance(res, tuple) else (False, (res,)) | ||
| res = tuple(_cast_annarray(r) if o is None else o for r, o in zip(res, out)) | ||
| for a, r in zip(ann_out, res): | ||
| if a is not None: | ||
| r.ann.update(a) | ||
| inputs_and_where = inputs + ((kwargs["where"],) if "where" in kwargs else ()) | ||
| if ufunc.signature is None or all( | ||
| map(lambda x: x in " (),->", ufunc.signature) | ||
| ): | ||
| if method in ("__call__", "reduceat", "accumulate") or ( | ||
| method == "reduce" and kwargs.get("keepdims", False) | ||
| ): | ||
| self._ufunc_call(inputs_and_where, res) | ||
| elif method == "reduce": | ||
| self._ufunc_reduce(inputs_and_where, res, kwargs.get("axis", 0)) | ||
| elif method == "at": | ||
| self._ufunc_at(inputs) | ||
| return None | ||
| elif method == "outer": | ||
| self._ufunc_outer(inputs, res, kwargs.get("where", True)) | ||
| else: | ||
| warnings.warn("unrecognized ufunc method", AnnotationWarning) | ||
| else: | ||
| res = self._gufunc_call(ufunc, inputs, kwargs, res) | ||
| res = tuple(r if isinstance(r, np.generic) else self.relax(r) for r in res) | ||
| return res if istuple else res[0] | ||
| @staticmethod | ||
| def _ufunc_call(inputs_and_where, res): | ||
| for out, in_ in itertools.product(res, inputs_and_where): | ||
| if not isinstance(out, AnnotatedArray): | ||
| continue | ||
| out.ann.update(getattr(in_, "ann", ())) | ||
| @staticmethod | ||
| def _ufunc_reduce(inputs_and_where, res, axis=0, keepdims=False): | ||
| out = res[0] # reduce only allowed for single output functions | ||
| if not isinstance(out, AnnotatedArray): | ||
| return | ||
| axis = axis if isinstance(axis, tuple) else (axis,) | ||
| in_ = inputs_and_where[0] | ||
| axis = sorted(map(lambda x: x % np.ndim(in_) - np.ndim(in_), axis)) | ||
| for in_ in inputs_and_where: | ||
| ann = list(getattr(in_, "ann", [])) | ||
| if not keepdims: | ||
| for a in axis: | ||
| try: | ||
| del ann[a] | ||
| except IndexError: | ||
| pass | ||
| out.ann.update(ann) | ||
| @staticmethod | ||
| def _ufunc_at(inputs): | ||
| out = inputs[0] | ||
| if not isinstance(out, AnnotatedArray): | ||
| return | ||
| if any(d != {} for d in getattr(inputs[1], "ann", ())): | ||
| warnings.warn("annotations in indices are ignored", AnnotationWarning) | ||
| for in_ in inputs[2:]: | ||
| ann = getattr(in_, "ann", ()) | ||
| out.ann.update(ann) | ||
| @staticmethod | ||
| def _ufunc_outer(inputs, res, where=True): | ||
| for out in res: | ||
| if not isinstance(out, AnnotatedArray): | ||
| continue | ||
| in_ann = tuple( | ||
| i for a in inputs for i in getattr(a, "ann", np.ndim(a) * ({},)) | ||
| ) | ||
| out.ann.update(in_ann) | ||
| where_ann = getattr(where, "ann", ()) | ||
| out.ann.update(where_ann) | ||
| @staticmethod | ||
| def _gufunc_call(ufunc, inputs, kwargs, res): | ||
| sigin, sigout = _parse_signature(ufunc.signature, map(np.ndim, inputs)) | ||
| if kwargs.get("keepdims", False): | ||
| sigout = [sigin[0] for _ in range(ufunc.nout)] | ||
| ndims = [np.ndim(i) for x in (inputs, res) for i in x] | ||
| axes = getattr(kwargs, "axes", None) | ||
| if axes is None: | ||
| axis = getattr(kwargs, "axis", None) | ||
| if axis is None: | ||
| axes = [tuple(range(-len(i), 0)) for i in sigin + sigout] | ||
| else: | ||
| axes = [(axis,) for _ in range(ufunc.nin)] | ||
| else: | ||
| axes = [ | ||
| tuple(a) if isinstance(a, collections.abc.Iterable) else (a,) | ||
| for a in axes | ||
| ] | ||
| append = axes[0] if kwargs.get("keepdims", False) else () | ||
| axes += [append] * (ufunc.nin + ufunc.nout - len(axes)) | ||
| axes = [(*(i % ndim - ndim for i in a),) for a, ndim in zip(axes, ndims)] | ||
| iterdims = [ | ||
| [i for i in range(-1, -1 - ndim, -1) if i not in a] | ||
| for a, ndim in zip(axes, ndims) | ||
| ] | ||
| # compare core dimensions | ||
| coredims = {} | ||
| for i, (ax, sig) in enumerate(zip(axes, sigin + sigout)): | ||
| for a, key in zip(ax, sig): | ||
| if key.isnumeric(): | ||
| continue | ||
| coredims.setdefault(key, []).append((i, a)) | ||
| inout = tuple(inputs) + res | ||
| for val in coredims.values(): | ||
| for (isrc, dimsrc), (idest, dimdest) in itertools.combinations(val, 2): | ||
| source = getattr(inout[isrc], "ann", {dimsrc: {}})[dimsrc] | ||
| dest = getattr(inout[idest], "ann", {dimdest: AnnotationDict()})[ | ||
| dimdest | ||
| ] | ||
| if isrc < ufunc.nin <= idest: | ||
| dest.update(source) | ||
| else: | ||
| dest.match(source) | ||
| # compare iteration dimensions | ||
| for iout, out in enumerate(res): | ||
| if not isinstance(out, AnnotatedArray): | ||
| continue | ||
| for idim, dim in enumerate(iterdims[ufunc.nin + iout]): | ||
| dest = out.ann[dim] | ||
| for in_, iterdim in zip(inputs, iterdims): | ||
| if idim >= len(iterdim) or getattr(in_, "ann", None) is None: | ||
| continue | ||
| source = getattr(in_, "ann", {iterdim[idim]: {}})[iterdim[idim]] | ||
| dest.update(source) | ||
| return res | ||
| def __array_function__(self, func, types, args, kwargs): | ||
| """Function calls on the array. | ||
| Calls defined function in :data:`HANDLED_FUNCTIONS` otherwise raises exception. | ||
| Add functions to it by using the decorator :func:`implements` for custom | ||
| implementations. | ||
| """ | ||
| if func not in HANDLED_FUNCTIONS: | ||
| return NotImplemented | ||
| # if not all(issubclass(t, self.__class__) for t in types): | ||
| # return NotImplemented | ||
| return HANDLED_FUNCTIONS[func](*args, **kwargs) | ||
| def __getitem__(self, key): | ||
| """Get an item from the AnnotatedArray. | ||
| The indexing supports most of numpys regular and fancy indexing. | ||
| """ | ||
| res = AnnotatedArray(self._array[key]) | ||
| if isinstance(res, np.generic) or res.ndim == 0: | ||
| return res | ||
| key = key if isinstance(key, tuple) else (key,) | ||
| key, fancy_ndim, prepend_fancy, lenellipsis = _parse_key(key, self.ndim) | ||
| source = 0 | ||
| dest = fancy_ndim if prepend_fancy else 0 | ||
| for k in key: | ||
| if k is not True and k is not False and isinstance(k, (int, np.integer)): | ||
| source += 1 | ||
| elif k is None: | ||
| dest += 1 | ||
| elif isinstance(k, slice): | ||
| for kk, val in self.ann[source].items(): | ||
| if kk in self._scales: | ||
| res.ann[dest][kk] = val[k] | ||
| else: | ||
| res.ann[dest][kk] = val | ||
| dest += 1 | ||
| source += 1 | ||
| elif k is Ellipsis: | ||
| for _ in range(lenellipsis): | ||
| res.ann[dest].update(self.ann[source]) | ||
| dest += 1 | ||
| source += 1 | ||
| else: | ||
| k = np.asanyarray(k) | ||
| ksq = k.squeeze() | ||
| if ksq.ndim == 1: | ||
| pos = (np.array(k.shape) == k.size).argmax() | ||
| ann = ( | ||
| self.ann[source + pos] if k.dtype == bool else self.ann[source] | ||
| ) | ||
| pos += int(not prepend_fancy) * dest + fancy_ndim - k.ndim | ||
| for kk, val in ann.items(): | ||
| if kk in self._scales: | ||
| res.ann[pos][kk] = val[ksq] | ||
| else: | ||
| res.ann[pos][kk] = val | ||
| source += k.ndim if k.dtype == bool else 1 | ||
| if not prepend_fancy: | ||
| dest += fancy_ndim | ||
| fancy_ndim = 0 | ||
| return self.relax(res) | ||
| def __setitem__(self, key, value): | ||
| """Set values. | ||
| If the provided value is an AnnotatedArray the annotations of corresponding | ||
| dimensions will be matched. | ||
| """ | ||
| self._array[key] = value | ||
| if not hasattr(value, "ann"): | ||
| return | ||
| key = key if isinstance(key, tuple) else (key,) | ||
| key, fancy_ndim, prepend_fancy, lenellipsis = _parse_key(key, self.ndim) | ||
| source = 0 | ||
| dest = fancy_ndim if prepend_fancy else 0 | ||
| for k in key: | ||
| if k is not True and k is not False and isinstance(k, (int, np.integer)): | ||
| source += 1 | ||
| elif k is None: | ||
| dest += 1 | ||
| elif isinstance(k, slice): | ||
| for kk, val in self.ann[source].items(): | ||
| if kk not in value.ann[dest]: | ||
| continue | ||
| if kk in self._scales: | ||
| val = val[k] | ||
| if val != value.ann[dest][kk]: | ||
| warnings.warn( | ||
| f"incompatible annotations with key '{kk}' " | ||
| f"comparing dimensions '{source}' and '{dest}'", | ||
| AnnotationWarning, | ||
| ) | ||
| dest += 1 | ||
| source += 1 | ||
| elif k is Ellipsis: | ||
| for _ in range(lenellipsis): | ||
| self.ann[source].match(value.ann[dest]) | ||
| dest += 1 | ||
| source += 1 | ||
| else: | ||
| k = np.asanyarray(k) | ||
| ksq = k.squeeze() | ||
| if ksq.ndim == 1: | ||
| pos = (np.array(k.shape) == k.size).argmax() | ||
| ann = ( | ||
| self.ann[source + pos] if k.dtype == bool else self.ann[source] | ||
| ) | ||
| pos += int(not prepend_fancy) * dest + fancy_ndim - k.ndim | ||
| for kk, val in ann.items(): | ||
| if kk not in value.ann[pos]: | ||
| continue | ||
| if kk in self._scales: | ||
| val = val[k] | ||
| if val != value.ann[pos][kk]: | ||
| warnings.warn( | ||
| f"incompatible annotations with key '{kk}' " | ||
| f"comparing dimensions '{source}' and '{pos}'", | ||
| AnnotationWarning, | ||
| ) | ||
| source += k.ndim if k.dtype == bool else 1 | ||
| if not prepend_fancy: | ||
| dest += fancy_ndim | ||
| fancy_ndim = 0 | ||
| @property | ||
| def T(self): | ||
| """Transpose. | ||
| See also :py:attr:`numpy.ndarray.T`. | ||
| """ | ||
| return self.transpose() | ||
| @implements(np.all) | ||
| def all(self, axis=None, dtype=None, out=None, keepdims=False, *, where=True): | ||
| """Test if all elements (along an axis) are True. | ||
| See also :py:meth:`numpy.ndarray.all`. | ||
| """ | ||
| return np.logical_and.reduce(self, axis, dtype, out, keepdims, where=where) | ||
| @implements(np.any) | ||
| def any(self, axis=None, dtype=None, out=None, keepdims=False, *, where=True): | ||
| """Test if any element (along an axis) is True. | ||
| See also :py:meth:`numpy.ndarray.any`. | ||
| """ | ||
| return np.logical_or.reduce(self, axis, dtype, out, keepdims, where=where) | ||
| @implements(np.max) | ||
| def max(self, axis=None, out=None, keepdims=False, initial=None, where=True): | ||
| """Maximum (along an axis). | ||
| See also :py:meth:`numpy.ndarray.max`. | ||
| """ | ||
| return np.maximum.reduce(self, axis, None, out, keepdims, initial, where) | ||
| @implements(np.min) | ||
| def min(self, axis=None, out=None, keepdims=False, initial=None, where=True): | ||
| """Minimum (along an axis). | ||
| See also :py:meth:`numpy.ndarray.min`. | ||
| """ | ||
| return np.minimum.reduce(self, axis, None, out, keepdims, initial, where) | ||
| @implements(np.sum) | ||
| def sum( | ||
| self, axis=None, dtype=None, out=None, keepdims=False, initial=0, where=True | ||
| ): | ||
| """Sum of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.sum`. | ||
| """ | ||
| return np.add.reduce(self, axis, dtype, out, keepdims, initial, where) | ||
| @implements(np.prod) | ||
| def prod( | ||
| self, axis=None, dtype=None, out=None, keepdims=False, initial=1, where=True | ||
| ): | ||
| """Product of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.prod`. | ||
| """ | ||
| return np.multiply.reduce(self, axis, dtype, out, keepdims, initial, where) | ||
| @implements(np.cumsum) | ||
| def cumsum(self, axis=None, dtype=None, out=None): | ||
| """Cumulative sum of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.cumsum`. | ||
| """ | ||
| if axis is None: | ||
| return np.add.accumulate(self.flatten(), 0, dtype, out) | ||
| return np.add.accumulate(self, axis, dtype, out) | ||
| @implements(np.cumprod) | ||
| def cumprod(self, axis=None, dtype=None, out=None): | ||
| """Cumulative product of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.cumprod`. | ||
| """ | ||
| if axis is None: | ||
| return np.multiply.accumulate(self.flatten(), 0, dtype, out) | ||
| return np.multiply.accumulate(self, axis, dtype, out) | ||
| def flatten(self, order="C"): | ||
| """Flatten array to one dimension. | ||
| See also :py:meth:`numpy.ndarray.flatten`. | ||
| """ | ||
| res = self.relax(self._array.flatten(order)) | ||
| if res.shape == self.shape: | ||
| res.ann.update(self.ann) | ||
| elif len(tuple(filter(lambda x: x != 1, self.shape))) == 1: | ||
| res.ann[0].update(self.ann[self.shape.index(res.size)]) | ||
| return res | ||
| @implements(np.trace) | ||
| def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): | ||
| """Trace of an array. | ||
| See also :py:meth:`numpy.ndarray.trac`. | ||
| """ | ||
| ann = tuple(a for i, a in enumerate(self.ann) if i not in (axis1, axis2)) | ||
| return self.relax(self._array.trace(offset, axis1, axis2, dtype, out), ann) | ||
| def astype(self, *args, **kwargs): | ||
| """Return array as given type. | ||
| See also :py:meth:`numpy.ndarray.astype`. | ||
| """ | ||
| return self.relax(self._array.astype(*args, **kwargs), self.ann) | ||
| @property | ||
| @implements(np.ndim) | ||
| def ndim(self): | ||
| """Number of array dimensions. | ||
| See also :py:attr:`numpy.ndarray.ndim`. | ||
| """ | ||
| return self._array.ndim | ||
| @property | ||
| @implements(np.shape) | ||
| def shape(self): | ||
| """Array shape. | ||
| See also :py:attr:`numpy.ndarray.shape`. | ||
| """ | ||
| return self._array.shape | ||
| @property | ||
| @implements(np.size) | ||
| def size(self): | ||
| """Array size. | ||
| See also :py:attr:`numpy.ndarray.size`. | ||
| """ | ||
| return self._array.size | ||
| @property | ||
| @implements(np.imag) | ||
| def imag(self): | ||
| """Imaginary part of the array. | ||
| See also :py:attr:`numpy.ndarray.imag`. | ||
| """ | ||
| return self.relax(self._array.imag, self.ann) | ||
| @property | ||
| @implements(np.real) | ||
| def real(self): | ||
| """Real part of the array. | ||
| See also :py:attr:`numpy.ndarray.real`. | ||
| """ | ||
| return self.relax(self._array.real, self.ann) | ||
| def conjugate(self, *args, **kwargs): | ||
| """Complex conjugate elementwise. | ||
| See also :py:meth:`numpy.ndarray.conjugate`. | ||
| """ | ||
| return np.conjugate(self, *args, **kwargs) | ||
| @implements(np.diagonal) | ||
| def diagonal(self, offset=0, axis1=0, axis2=1): | ||
| """Get the diagonal of the array. | ||
| See also :py:meth:`numpy.ndarray.diagonal`. | ||
| """ | ||
| ann = tuple(a for i, a in enumerate(self.ann) if i not in (axis1, axis2)) + ( | ||
| {}, | ||
| ) | ||
| return self.relax(self._array.diagonal(offset, axis1, axis2), ann) | ||
| @implements(np.transpose) | ||
| def transpose(self, axes=None): | ||
| """Transpose array dimensions. | ||
| See also :py:meth:`numpy.ndarray.transpose`. | ||
| """ | ||
| axes = range(self.ndim - 1, -1, -1) if axes is None else axes | ||
| return self.relax(self._array.transpose(axes), self.ann[axes]) | ||
| conj = conjugate | ||
| @implements(np.swapaxes) | ||
| def swapaxes(self, axis1, axis2): | ||
| axis1 = axis1 % self.ndim | ||
| axis2 = axis2 % self.ndim | ||
| opp = {axis1: axis2, axis2: axis1} | ||
| axes = [opp.get(i, i) for i in range(self.ndim)] | ||
| return self.relax(self._array.swapaxes(axis1, axis2), self.ann[axes]) | ||
| def __matmul__(self, other): | ||
| if isinstance(other, _op.Operator): | ||
| return NotImplemented | ||
| return super().__matmul__(other) | ||
| @implements(np.linalg.solve) | ||
| def solve(a, b): | ||
| """Solve linear system. | ||
| See also :py:func:`numpy.linalg.solve`. | ||
| """ | ||
| if issubclass(type(a), type(b)) or ( | ||
| not issubclass(type(b), type(a)) and isinstance(a, AnnotatedArray) | ||
| ): | ||
| restype = type(a) | ||
| else: | ||
| restype = type(b) | ||
| res = AnnotatedArray(np.linalg.solve(np.asanyarray(a), np.asanyarray(b))) | ||
| a_ann = list(getattr(a, "ann", [{}, {}])) | ||
| b_ann = list(getattr(b, "ann", [{}, {}])) | ||
| if np.ndim(b) == np.ndim(a) - 1: | ||
| map(lambda x: x[0].match(x[1]), zip(a_ann[-2::-1], b_ann[-1::-1])) | ||
| del a_ann[-2] | ||
| del b_ann[-1] | ||
| else: | ||
| map(lambda x: x[0].match(x[1]), zip(a_ann[-2::-1], b_ann[-2::-1])) | ||
| del a_ann[-2] | ||
| del b_ann[-2] | ||
| a_ann += [{}] | ||
| res.ann.update(a_ann) | ||
| res.ann.update(b_ann) | ||
| return restype.relax(res) | ||
| @implements(np.linalg.lstsq) | ||
| def lstsq(a, b, rcond="warn"): | ||
| """Solve linear system using least squares. | ||
| See also :py:func:`numpy.linalg.lstsq`. | ||
| """ | ||
| if issubclass(type(a), type(b)) or ( | ||
| not issubclass(type(b), type(a)) and isinstance(a, AnnotatedArray) | ||
| ): | ||
| restype = type(a) | ||
| else: | ||
| restype = type(b) | ||
| res = list(np.linalg.lstsq(np.asanyarray(a), np.asanyarray(b), rcond)) | ||
| res[0] = AnnotatedArray(res[0]) | ||
| a_ann = getattr(a, "ann", ({},)) | ||
| b_ann = getattr(b, "ann", ({},)) | ||
| a_ann[0].match(b_ann[0]) | ||
| res[0].ann[0].update(a_ann[-1]) | ||
| if np.ndim(b) == 2: | ||
| res[0].ann[1].update(b_ann[-1]) | ||
| res[0] = restype.relax(res[0]) | ||
| return tuple(res) | ||
| @implements(np.linalg.svd) | ||
| def svd(a, full_matrices=True, compute_uv=True, hermitian=False): | ||
| """Compute the singular value decomposition. | ||
| See also :py:func:`numpy.linalg.svd`. | ||
| """ | ||
| res = list(np.linalg.svd(np.asanyarray(a), full_matrices, compute_uv, hermitian)) | ||
| ann = getattr(a, "ann", ({},)) | ||
| if compute_uv: | ||
| res[0] = a.relax(res[0], ann[:-1] + ({},)) | ||
| res[1] = a.relax(res[1], ann[:-2] + ({},)) | ||
| res[2] = a.relax(res[2], ann[:-2] + ({}, ann[-1])) | ||
| return res | ||
| return a.relax(res, tuple(ann[:-2]) + ({},)) | ||
| @implements(np.diag) | ||
| def diag(a, k=0): | ||
| """Extract diagonal from an array or create an diagonal array. | ||
| See also :py:func:`numpy.diag`. | ||
| """ | ||
| res = np.diag(np.asanyarray(a), k) | ||
| ann = a.ann | ||
| if a.ndim == 1: | ||
| ann = (ann[0], copy.copy(ann[0])) | ||
| elif k == 0: | ||
| ann[0].match(ann[1]) | ||
| ann = ({**ann[0], **ann[1]},) | ||
| else: | ||
| ann = () | ||
| return a.relax(res, ann) | ||
| @implements(np.tril) | ||
| def tril(a, k=0): | ||
| """Get lower trinagular matrix. | ||
| See also :py:func:`numpy.tril`. | ||
| """ | ||
| res = np.tril(np.asanyarray(a), k) | ||
| return a.relax(res, getattr(a, "ann", ({},))) | ||
| @implements(np.triu) | ||
| def triu(a, k=0): | ||
| """Get upper trinagular matrix. | ||
| See also :py:func:`numpy.triu`. | ||
| """ | ||
| res = np.triu(np.asanyarray(a), k) | ||
| return a.relax(res, getattr(a, "ann", ({},))) | ||
| @implements(np.zeros_like) | ||
| def zeros_like(a, dtype=None, order="K", shape=None): | ||
| """Create a numpy array like the given array containing zeros.""" | ||
| return np.zeros_like(np.asarray(a), dtype=dtype, order=order, shape=shape) | ||
| @implements(np.ones_like) | ||
| def ones_like(a, dtype=None, order="K", shape=None): | ||
| """Create a numpy array like the given array containing ones.""" | ||
| return np.ones_like(np.asarray(a), dtype=dtype, order=order, shape=shape) |
+5
| Wheel-Version: 1.0 | ||
| Generator: setuptools (70.1.1) | ||
| Root-Is-Purelib: false | ||
| Tag: cp310-cp310-macosx_11_0_arm64 | ||
Sorry, the diff of this file is not supported yet
| name: build | ||
| on: | ||
| push: | ||
| tags: | ||
| - v* | ||
| jobs: | ||
| build_wheels: | ||
| name: Build wheels on ${{ matrix.os }} | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Setup msys2 | ||
| if: matrix.os == 'windows-latest' | ||
| uses: msys2/setup-msys2@v2 | ||
| with: | ||
| update: true | ||
| install: mingw-w64-x86_64-gcc | ||
| - name: Set path | ||
| if: matrix.os == 'windows-latest' | ||
| run: echo "$env:RUNNER_TEMP\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | ||
| - name: Build wheels | ||
| uses: pypa/cibuildwheel@v2.13.1 | ||
| - uses: actions/upload-artifact@v3 | ||
| with: | ||
| path: ./wheelhouse/*.whl | ||
| build_sdist: | ||
| name: Build source distribution | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Build sdist | ||
| run: pipx run build --sdist | ||
| - uses: actions/upload-artifact@v3 | ||
| with: | ||
| path: dist/*.tar.gz |
| name: docs | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| jobs: | ||
| docs: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| fetch-depth: 0 | ||
| - uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Build treams | ||
| run: python -m pip install -e . | ||
| - name: Build html | ||
| run: | | ||
| python -m pip install treams[docs] | ||
| sphinx-build -b html docs docs/_build/html | ||
| - name: Upload artifacts | ||
| uses: actions/upload-artifact@v3 | ||
| with: | ||
| name: html-docs | ||
| path: docs/_build/html/ | ||
| - name: Deploy docs | ||
| uses: peaceiris/actions-gh-pages@v3 | ||
| if: github.ref == 'refs/heads/main' | ||
| with: | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| publish_dir: docs/_build/html |
| name: doctests | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| jobs: | ||
| doctests: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| fetch-depth: 0 | ||
| - uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Build treams | ||
| run: python -m pip install -e . | ||
| - name: Run doctests | ||
| run: | | ||
| python -m pip install treams[docs,io] | ||
| sphinx-build -b doctest docs docs/_build/doctest |
| name: tests | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| jobs: | ||
| tests: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| pull-requests: write | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@v3 | ||
| with: | ||
| fetch-depth: 0 | ||
| - uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.11' | ||
| - name: Build treams with tracing | ||
| run: CYTHON_COVERAGE=1 python -m pip install -e . | ||
| - name: Run tests | ||
| run: | | ||
| python -m pip install treams[test,coverage,io] | ||
| python -m pytest tests/unit --cov src/treams --cov-report html | ||
| - name: Get coverage report | ||
| run: | | ||
| python -m coverage json | ||
| export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") | ||
| echo -n "{ \"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"$TOTAL%\", \"color\": \"" > htmlcov/endpoint.json | ||
| if [ "$TOTAL" -lt 60 ]; then echo -n "red" >> htmlcov/endpoint.json; \ | ||
| elif [ "$TOTAL" -lt 70 ]; then echo -n "orange" >> htmlcov/endpoint.json; \ | ||
| elif [ "$TOTAL" -lt 80 ]; then echo -n "yellow" >> htmlcov/endpoint.json; \ | ||
| elif [ "$TOTAL" -lt 90 ]; then echo -n "yellowgreen" >> htmlcov/endpoint.json; \ | ||
| elif [ "$TOTAL" -lt 95 ]; then echo -n "green" >> htmlcov/endpoint.json; \ | ||
| else echo -n "brightgreen" >> htmlcov/endpoint.json; fi | ||
| echo "\" }" >> htmlcov/endpoint.json | ||
| rm htmlcov/.gitignore | ||
| - name: Deploy report | ||
| uses: s0/git-publish-subdir-action@develop | ||
| env: | ||
| REPO: self | ||
| BRANCH: htmlcov | ||
| FOLDER: htmlcov | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
-38
| """Configuration for pytest. | ||
| It's needed here to add the option `--runslow`, which is mainly used in the lattice | ||
| subpackage. | ||
| """ | ||
| import pytest | ||
| def pytest_addoption(parser): | ||
| """Add option '--runslow' and '--rungmsh'.""" | ||
| parser.addoption( | ||
| "--runslow", action="store_true", default=False, help="run slow tests" | ||
| ) | ||
| parser.addoption( | ||
| "--rungmsh", action="store_true", default=False, help="run tests needing gmsh" | ||
| ) | ||
| def pytest_configure(config): | ||
| """Add marker 'slow' and 'gmsh'.""" | ||
| config.addinivalue_line("markers", "gmsh: test needs gmsh") | ||
| config.addinivalue_line("markers", "slow: mark test as slow to run") | ||
| def pytest_collection_modifyitems(config, items): | ||
| """Skip tests without necessary options.""" | ||
| if not config.getoption("--runslow"): | ||
| # --runslow not given in cli: skip slow tests | ||
| skip_slow = pytest.mark.skip(reason="need --runslow option to run") | ||
| for item in items: | ||
| if "slow" in item.keywords: | ||
| item.add_marker(skip_slow) | ||
| if not config.getoption("--rungmsh"): | ||
| # --rungmsh not given in cli: skip gmsh tests | ||
| skip_slow = pytest.mark.skip(reason="need --rungmsh option to run") | ||
| for item in items: | ||
| if "gmsh" in item.keywords: | ||
| item.add_marker(skip_slow) |
| @import 'pyramid.css'; | ||
| body { | ||
| font-family: sans-serif; | ||
| } | ||
| table.align-default { | ||
| margin-left: 0; | ||
| } |
| {{ fullname | escape | underline}} | ||
| .. autoattribute:: {{ fullname }} |
| {{ fullname | escape | underline}} | ||
| .. automodule:: {{ fullname }} | ||
| :no-members: |
| {{ fullname }} | ||
| {{ underline }} | ||
| .. currentmodule:: {{ module }} | ||
| .. autoclass:: {{ objname }} | ||
| {% block attributes %} | ||
| {% if attributes %} | ||
| .. rubric:: Attributes | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in all_attributes %} | ||
| {%- if not item.startswith('_') %} | ||
| ~{{ name }}.{{ item }} | ||
| {%- endif -%} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} | ||
| {% block methods %} | ||
| .. rubric:: Methods | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in all_methods %} | ||
| {%- if not item.startswith('_') or item in ['__call__', '__getitem__', '__setitem__', '__len__', '__repr__', '__str__', '__array__', '__array_ufunc__', '__array_function__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__add__', '__sub__', '__mul__', '__matmul__', '__truediv__', '__floordiv__', '__mod__', '__divmod__', '__pow__', '__lshift__', '__rshift__', '__and__', '__xor__', '__or__', '__neg__', '__pos__', '__abs__', '__invert__'] %} | ||
| ~{{ name }}.{{ item }} | ||
| {%- endif -%} | ||
| {%- endfor %} | ||
| {% for item in inherited_members %} | ||
| {%- if item in ['__call__', '__getitem__', '__setitem__', '__len__', '__repr__', '__str__', '__array__', '__array_ufunc__', '__array_function__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__add__', '__sub__', '__mul__', '__matmul__', '__truediv__', '__floordiv__', '__mod__', '__divmod__', '__pow__', '__lshift__', '__rshift__', '__and__', '__xor__', '__or__', '__neg__', '__pos__', '__abs__', '__invert__'] %} | ||
| ~{{ name }}.{{ item }} | ||
| {%- endif -%} | ||
| {%- endfor %} | ||
| {% endblock %} |
| {{ fullname | escape | underline}} | ||
| .. automodule:: {{ fullname }} | ||
| {% block functions %} | ||
| {% if members %} | ||
| .. rubric:: {{ _('Functions') }} | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in members %} | ||
| {%- if not item.startswith('__') %} | ||
| {{ item }} | ||
| {%- endif -%} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} |
| {{ fullname | escape | underline}} | ||
| .. automodule:: {{ fullname }} | ||
| {% block attributes %} | ||
| {% if attributes %} | ||
| .. rubric:: {{ _('Module Attributes') }} | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in attributes %} | ||
| {{ item }} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} | ||
| {% block functions %} | ||
| {% if functions %} | ||
| .. rubric:: {{ _('Functions') }} | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in functions %} | ||
| {{ item }} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} | ||
| {% block classes %} | ||
| {% if classes %} | ||
| .. rubric:: {{ _('Classes') }} | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in classes %} | ||
| {{ item }} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} | ||
| {% block exceptions %} | ||
| {% if exceptions %} | ||
| .. rubric:: {{ _('Exceptions') }} | ||
| .. autosummary:: | ||
| :toctree: | ||
| {% for item in exceptions %} | ||
| {{ item }} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} | ||
| {% block modules %} | ||
| {% if modules %} | ||
| .. rubric:: Modules | ||
| .. autosummary:: | ||
| :toctree: | ||
| :recursive: | ||
| {% for item in modules %} | ||
| {{ item }} | ||
| {%- endfor %} | ||
| {% endif %} | ||
| {% endblock %} |
| ===== | ||
| About | ||
| ===== | ||
| treams is developed at the `Karlsruhe Institute of Technology <https://www.kit.edu>`_ by | ||
| the | ||
| `Institute of Theoretical Solid State Physics <https://www.tfp.kit.edu/rockstuhl.php>`_. | ||
| Publications | ||
| ============ | ||
| The following publications document the developments and methods for different parts of | ||
| the code. | ||
| 1. `D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, treams -- A T-matrix scattering code for nanophotonic computations, arXiv (preprint), 2309.03182 (2023). <https://doi.org/10.48550/arXiv.2309.03182>`_ | ||
| 2. `D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, Unified Lattice Sums Accommodating Multiple Sublattices for Solutions of the Helmholtz Equation in Two and Three Dimensions, Phys. Rev. A 107, 013508 (2023). <https://doi.org/10.1103/PhysRevA.107.013508>`_ | ||
| 3. `D. Beutel, P. Scott, M. Wegener, C. Rockstuhl, and I. Fernandez-Corbaton, Enhancing the Optical Rotation of Chiral Molecules Using Helicity Preserving All-Dielectric Metasurfaces, Appl. Phys. Lett. 118, 221108 (2021). <https://doi.org/10.1063/5.0050411>`_ | ||
| 4. `D. Beutel, A. Groner, C. Rockstuhl, C. Rockstuhl, and I. Fernandez-Corbaton, Efficient Simulation of Biperiodic, Layered Structures Based on the T-Matrix Method, J. Opt. Soc. Am. B, JOSAB 38, 1782 (2021). <https://doi.org/10.1364/JOSAB.419645>`_ |
-130
| """Sphinx config.""" | ||
| # Configuration file for the Sphinx documentation builder. | ||
| # | ||
| # This file only contains a selection of the most common options. For a full | ||
| # list see the documentation: | ||
| # https://www.sphinx-doc.org/en/master/usage/configuration.html | ||
| # -- Path setup -------------------------------------------------------------- | ||
| # If extensions (or modules to document with autodoc) are in another directory, | ||
| # add these directories to sys.path here. If the directory is relative to the | ||
| # documentation root, use os.path.abspath to make it absolute, like shown here. | ||
| # | ||
| from importlib.metadata import version | ||
| import numpy as np | ||
| # sys.path.insert(0, os.path.abspath('..')) | ||
| # -- Project information ----------------------------------------------------- | ||
| project = "treams" | ||
| copyright = "2023, Dominik Beutel" | ||
| author = "Dominik Beutel" | ||
| release = version("treams") | ||
| version = ".".join(release.split(".")[:3]) | ||
| # -- General configuration --------------------------------------------------- | ||
| # Add any Sphinx extension module names here, as strings. They can be | ||
| # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | ||
| # ones. | ||
| extensions = [ | ||
| "sphinx.ext.autodoc", | ||
| "sphinx.ext.autosectionlabel", | ||
| "sphinx.ext.autosummary", | ||
| "sphinx.ext.coverage", | ||
| "sphinx.ext.doctest", | ||
| "sphinx.ext.intersphinx", | ||
| "sphinx.ext.mathjax", | ||
| "sphinx.ext.napoleon", | ||
| "sphinx.ext.todo", | ||
| "matplotlib.sphinxext.plot_directive", | ||
| ] | ||
| autosectionlabel_prefix_document = True | ||
| autosummary_filename_map = { | ||
| "treams.Lattice": "treams.Lattice_class", | ||
| "treams.Lattice.reciprocal": "treams.Lattice_class.reciprocal", | ||
| "treams.Lattice.volume": "treams.Lattice_class.volume", | ||
| } | ||
| doctest_global_setup = """ | ||
| import numpy as np | ||
| from treams import * | ||
| np.set_printoptions(precision=3, suppress=True) | ||
| """ | ||
| intersphinx_mapping = { | ||
| "scipy": ("https://docs.scipy.org/doc/scipy", None), | ||
| "numpy": ("https://numpy.org/doc/stable", None), | ||
| } | ||
| todo_include_todos = False | ||
| # Add any paths that contain templates here, relative to this directory. | ||
| templates_path = ["_templates"] | ||
| # List of patterns, relative to source directory, that match files and | ||
| # directories to ignore when looking for source files. | ||
| # This pattern also affects html_static_path and html_extra_path. | ||
| exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] | ||
| # -- Options for HTML output ------------------------------------------------- | ||
| # The theme to use for HTML and HTML Help pages. See the documentation for | ||
| # a list of builtin themes. | ||
| # | ||
| html_theme = "pyramid" | ||
| html_css_files = ["custom.css"] | ||
| html_sidebars = { | ||
| "**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"] | ||
| } | ||
| # Add any paths that contain custom static files (such as style sheets) here, | ||
| # relative to this directory. They are copied after the builtin static files, | ||
| # so a file named "default.css" will overwrite the builtin "default.css". | ||
| html_static_path = ["_static"] | ||
| def preprocess_ufunc(app, what, name, obj, options, lines): | ||
| """Remove the first four docstring lines from numpy ufuncs. | ||
| The docstring of ufuncs have a line containing the function name and signature and a | ||
| blank line automatically prepended. The signature contains all arguments and keyword | ||
| arguments but the positional arguments only get dummy variable names ('x1', 'x2', | ||
| ...) and is therefore often confusing. | ||
| By convention all ufuncs contain in their first manually added lines the function | ||
| name with a simplified signature followed by a blank line. We explicitly add the | ||
| signature with a separate process to autodoc. | ||
| The four lines containing function names and signatures or being blank are therefore | ||
| removed for a clean documentation produced by sphinx. | ||
| """ | ||
| if isinstance(obj, np.ufunc): | ||
| lines.pop(0) | ||
| lines.pop(0) | ||
| lines.pop(0) | ||
| lines.pop(0) | ||
| def ufunc_signature(app, what, name, obj, options, signature, return_annotation): | ||
| """Extract the signature from the docstring for numpy ufuncs. | ||
| As described in :func:`preprocess_ufunc` the third line in a ufunc docstring | ||
| contains the manually added function name and signature intended to be displayed in | ||
| the documentation (the preceeding two lines are automatically added). We extract the | ||
| signature and add it manually. | ||
| """ | ||
| if isinstance(obj, np.ufunc): | ||
| signature = "(" + obj.__doc__.split("\n")[2].split("(")[1] | ||
| return signature, return_annotation | ||
| def setup(app): | ||
| """Add processes to autodoc.""" | ||
| app.connect("autodoc-process-docstring", preprocess_ufunc) | ||
| app.connect("autodoc-process-signature", ufunc_signature) |
-153
| .. highlight:: console | ||
| ============================ | ||
| Development and contributing | ||
| ============================ | ||
| Setting up the development environment | ||
| ====================================== | ||
| 1. | ||
| Clone the repository with :: | ||
| git clone git@github.com:tfp-photonics/treams.git | ||
| or :: | ||
| git clone https://github.com/tfp-photonics/treams.git | ||
| and enter the directory :: | ||
| cd treams | ||
| 2. | ||
| This step is optional, but you might want to create an environment where treams and the | ||
| development tools are created :: | ||
| conda env create --name treams-dev python | ||
| Activate the environment with:: | ||
| conda activate treams-dev | ||
| 3. | ||
| Setup the package with :: | ||
| pip install -e . | ||
| This last step makes the program available in the environment independently of the | ||
| current folder. This is especially necessary for correctly building the documentation. | ||
| Running tests | ||
| ============= | ||
| To install the required packages for testing use :: | ||
| pip install treams[test,coverage,io] | ||
| Tests can be run using pytest with :: | ||
| python -m pytest | ||
| but for development a more fine grained selection can be made by passing a directory or | ||
| file as an argument. Additionally, the option ``-k`` allows to define keywords when | ||
| selecting test. | ||
| Some integration tests for the module :ref:`generated/treams.lattice:treams.lattice` | ||
| take a long time to finish and are therefore disabled by default. You can add them with | ||
| the option ``--runslow``. | ||
| If coverage reports should be included one can use the option ``--cov src/treams`` | ||
| (which requires pytest-cov to be installed). However, this will only report on the pure | ||
| python files. To also get coverage reports for the cython part, it is necessary to | ||
| compile it with linetracing support. This can be achieved by setting the environment | ||
| variable ``CYTHON_COVERAGE``, for example with :: | ||
| CYTHON_COVERAGE=1 pip install -e . | ||
| Make sure that new C code files are generated and that those files are compiled. | ||
| Enabling the tracing for getting code coverage slows down the integration tests | ||
| considerably. For the coverage calculation only the unit tests are used. | ||
| Some test for importing and exporting need gmsh. These tests can be activated with | ||
| ``--rungmsh`` analogous to the option ``--runslow`` above. | ||
| Building the documentation | ||
| ========================== | ||
| To install the required packages for testing use :: | ||
| pip install treams[docs] | ||
| The documentation is built with sphinx by using :: | ||
| sphinx-build -b html docs docs/_build/html | ||
| from the root directory of the package to build the documentation as html pages. | ||
| Some figures are automatically created, which requires matplotlib. | ||
| The doctests can be run with :: | ||
| sphinx-build -b doctest docs docs/_build/doctest | ||
| Building the code on Windows | ||
| ============================ | ||
| The main issue with using treams on Windows is the compilation step. For Windows Python | ||
| is usually compiled with MSVC for Visual Studio. However, especially for calculations | ||
| with complex numbers, Cython creates code that conforms to the (C99-) standard. Thus, it | ||
| is not compatible with the non-standard implementation of complex numbers by Microsoft. | ||
| Below you find three tested ways, how one can use treams with Windows. The first two | ||
| ways actually use a non-Windows version of Python, but have a more straightforward | ||
| installation procedure. However, the last one works with Windows' version of Python e.g. | ||
| when installed with conda under Windows. | ||
| Windows Subsystem for Linux | ||
| --------------------------- | ||
| The | ||
| `Windows Subsystem for Linux (WSL) <https://docs.microsoft.com/en-us/windows/wsl/install>`_ | ||
| exists on recent versions of Windows. To install it, open the command line interpreter | ||
| (cmd.exe) and type `wsl --install`. This step might be enough to be able to run a Linux | ||
| kernel. Within WSL all instructions from the rest of the description can be used, e.g. | ||
| with the distribution's Python or a conda-installed Python. | ||
| Sometimes it is necessary to use | ||
| `additional steps <https://docs.microsoft.com/en-us/windows/wsl/install-manual>`_ to | ||
| install WSL. | ||
| Pure MSYS2 | ||
| ---------- | ||
| Using `MSYS2 <https://www.msys2.org/>`_, it is also possible to compile and install | ||
| treams. First, install MSYS2 and update it according to the instructions. Then, also | ||
| install python and, if you want, the dependencies of treams. Otherwise, the dependencies | ||
| are installed by pip. | ||
| Compilation with mingw-w64 | ||
| -------------------------- | ||
| This is approach is different from the others, since it finally combines binaries from | ||
| two different compilers. Although it works and was tested on some systems, it is not | ||
| guaranteed that it will work for all systems. The following part describes, how treams | ||
| can be built for Windows. | ||
| After installing MSYS2 use it to install ``mingw-w64-x86_64-gcc``. | ||
| The compilation is steered from the command line. First go into the directory of treams. | ||
| Then, set up your path by prepending the direction for MSYS2's mingw64 binaries with | ||
| ``set PATH=C:\msys64\mingw64\bin;%PATH%`` (adjust accordingly if you have installed | ||
| MSYS2 with non-default parameters). Check that gcc from MSYS2 is recognized correctly | ||
| but make sure that the version of python that is found on the path corresponds to the | ||
| Windows Python. With this setup, building binaries should work with `python -m build`. | ||
| Continuous integration | ||
| ====================== | ||
| Using Github actions the steps above (building the documentation, running the doctests, | ||
| running the tests with a coverage report, and building for all platforms) are | ||
| implemented to run automatically on specific triggers. The relevant files in the | ||
| ``.gihub/workflows`` directory can be also used as examples in addition to the | ||
| description above. |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(1 / 600, 1 / 350, 100) | ||
| material_slab = 3 | ||
| thickness = 10 | ||
| material_sphere = (4, 1, 0.05) | ||
| pitch = 500 | ||
| lattice = treams.Lattice.square(pitch) | ||
| radius = 100 | ||
| lmax = mmax = 3 | ||
| tr = np.zeros((len(k0s), 2)) | ||
| for i, k0 in enumerate(k0s): | ||
| kpar = [0, 0.3 * k0] | ||
| spheres = treams.TMatrix.sphere( | ||
| lmax, k0, radius, [material_sphere, 1] | ||
| ).latticeinteraction.solve(pitch, kpar[0]) | ||
| cwb = treams.CylindricalWaveBasis.diffr_orders(kpar[0], mmax, pitch, 0.02) | ||
| spheres_cw = treams.TMatrixC.from_array(spheres, cwb) | ||
| chain_cw = spheres_cw.latticeinteraction.solve(pitch, kpar[1]) | ||
| pwb = treams.PlaneWaveBasisByComp.diffr_orders(kpar, lattice, 0.02) | ||
| plw = treams.plane_wave(kpar, [1, 0, 0], k0=k0, basis=pwb, material=1) | ||
| slab = treams.SMatrices.slab(thickness, pwb, k0, [1, material_slab, 1]) | ||
| dist = treams.SMatrices.propagation([0, 0, radius], pwb, k0, 1) | ||
| array = treams.SMatrices.from_array(chain_cw, pwb) | ||
| total = treams.SMatrices.stack([slab, dist, array]) | ||
| tr[i, :] = total.tr(plw) | ||
| fig, ax = plt.subplots() | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[:, 0]) | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[:, 1]) | ||
| ax.legend(["$T$", "$R$"]) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(1 / 600, 1 / 350, 100) | ||
| material_slab = 3 | ||
| thickness = 10 | ||
| material_sphere = (4, 1, 0.05) | ||
| lattice = treams.Lattice.square(500) | ||
| radius = 100 | ||
| lmax = 3 | ||
| tr = np.zeros((len(k0s), 2)) | ||
| for i, k0 in enumerate(k0s): | ||
| kpar = [0, 0.3 * k0] | ||
| spheres = treams.TMatrix.sphere( | ||
| lmax, k0, radius, [material_sphere, 1] | ||
| ).latticeinteraction.solve(lattice, kpar) | ||
| pwb = treams.PlaneWaveBasisByComp.diffr_orders(kpar, lattice, 0.02) | ||
| plw = treams.plane_wave(kpar, [1, 0, 0], k0=k0, basis=pwb, material=1) | ||
| slab = treams.SMatrices.slab(thickness, pwb, k0, [1, material_slab, 1]) | ||
| dist = treams.SMatrices.propagation([0, 0, radius], pwb, k0, 1) | ||
| array = treams.SMatrices.from_array(spheres, pwb) | ||
| total = treams.SMatrices.stack([slab, dist, array]) | ||
| tr[i, :] = total.tr(plw) | ||
| fig, ax = plt.subplots() | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[:, 0]) | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[:, 1]) | ||
| ax.legend(["$T$", "$R$"]) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(1 / 10_000, 1 / 350, 200) | ||
| material_slab = 3 | ||
| thickness = 10 | ||
| material_sphere = (4 + 0.1j, 1, 0.05) | ||
| lattice = treams.Lattice.square(500) | ||
| radius = 100 | ||
| lmax = 3 | ||
| az = 210 | ||
| res = [] | ||
| for i, k0 in enumerate(k0s): | ||
| kpar = [0, 0] | ||
| spheres = treams.TMatrix.sphere( | ||
| lmax, k0, radius, [material_sphere, 1] | ||
| ).latticeinteraction.solve(lattice, kpar) | ||
| pwb = treams.PlaneWaveBasisByComp.diffr_orders(kpar, lattice, 0.02) | ||
| plw = treams.plane_wave(kpar, [1, 0, 0], k0=k0, basis=pwb, material=1) | ||
| slab = treams.SMatrices.slab(thickness, pwb, k0, [1, material_slab, 1]) | ||
| dist = treams.SMatrices.propagation([0, 0, radius], pwb, k0, 1) | ||
| array = treams.SMatrices.from_array(spheres, pwb) | ||
| total = treams.SMatrices.stack([slab, dist, array, dist]) | ||
| x, _ = total.bands_kz(az) | ||
| x = x * az / np.pi | ||
| sel = x[np.abs(np.imag(x)) < 0.1] | ||
| res.append(sel) | ||
| fig, ax = plt.subplots() | ||
| for k0, sel in zip(k0s, res): | ||
| ax.scatter(sel.real, len(sel) * [299792.458 * k0 / (2 * np.pi)], 0.2, c="C0") | ||
| ax.scatter(sel.imag, len(sel) * [299792.458 * k0 / (2 * np.pi)], 0.2, c="C1") | ||
| ax.set_xlabel("$k_z a_z / \\pi$") | ||
| ax.set_ylabel("Frequency (THz)") | ||
| ax.set_xlim([-1, 1]) | ||
| ax.set_ylim(ymin=0) | ||
| ax.legend(["$Real$", "$Imag$"]) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0 = 2 * np.pi / 700 | ||
| materials = [treams.Material(-16.5 + 1j), treams.Material()] | ||
| lmax = mmax = 3 | ||
| radii = [75, 65] | ||
| positions = [[-30, 0, -75], [30, 0, 75]] | ||
| lattice = treams.Lattice(300) | ||
| kz = 0.005 | ||
| spheres = [treams.TMatrix.sphere(lmax, k0, r, materials) for r in radii] | ||
| chain = treams.TMatrix.cluster(spheres, positions).latticeinteraction.solve(lattice, kz) | ||
| bmax = 3.1 * lattice.reciprocal | ||
| cwb = treams.CylindricalWaveBasis.diffr_orders(kz, mmax, lattice, bmax, 2, positions) | ||
| chain_tmc = treams.TMatrixC.from_array(chain, cwb) | ||
| inc = treams.plane_wave( | ||
| [np.sqrt(k0 ** 2 - kz ** 2), 0, kz], | ||
| [np.sqrt(0.5), np.sqrt(0.5)], | ||
| k0=chain.k0, | ||
| material=chain.material, | ||
| ) | ||
| sca = chain_tmc @ inc.expand(chain_tmc.basis) | ||
| grid = ( | ||
| np.mgrid[-300:300:61j, 0:1, -150:150:31j].squeeze().transpose((1, 2, 0))[:, :-1, :] | ||
| ) | ||
| ez = np.zeros_like(grid[..., 0], complex) | ||
| valid = chain_tmc.valid_points(grid, radii) | ||
| ez[valid] = (inc.efield(grid[valid]) + sca.efield(grid[valid]))[..., 2] | ||
| ez[~valid] = np.nan | ||
| sca = chain @ inc.expand(chain.basis) | ||
| valid = ~valid & chain.valid_points(grid, radii) | ||
| vals = [] | ||
| for i, r in enumerate(grid[valid]): | ||
| swb = treams.SphericalWaveBasis.default(1, positions=[r]) | ||
| field = sca.expandlattice(basis=swb).efield(r) | ||
| vals.append(inc.efield(r)[2] + field[2]) | ||
| ez[valid] = vals | ||
| ez = np.concatenate( | ||
| ( | ||
| np.real(ez.T * np.exp(-1j * kz * lattice[...])), | ||
| ez.T.real, | ||
| np.real(ez.T * np.exp(1j * kz * lattice[...])), | ||
| ) | ||
| ) | ||
| zaxis = np.concatenate((grid[0, :, 2] - 300, grid[0, :, 2], grid[0, :, 2] + 300,)) | ||
| fig, ax = plt.subplots(figsize=(10, 20)) | ||
| pcm = ax.pcolormesh(grid[:, 0, 0], zaxis, ez, shading="nearest", vmin=-1, vmax=1,) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("$E_z$") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| ax.set_aspect("equal") | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0 = 2 * np.pi / 700 | ||
| materials = [treams.Material(-16.5 + 1j), treams.Material()] | ||
| lmax = 3 | ||
| radii = [75, 75] | ||
| positions = [[-30, 0, -75], [30, 0, 75]] | ||
| lattice = 300 | ||
| kz = 0 | ||
| spheres = [treams.TMatrix.sphere(lmax, k0, r, materials) for r in radii] | ||
| chain = treams.TMatrix.cluster(spheres, positions).latticeinteraction.solve(lattice, kz) | ||
| inc = treams.plane_wave([k0, 0, 0], [0, 0, 1], k0=chain.k0, material=chain.material) | ||
| sca = chain @ inc.expand(chain.basis) | ||
| grid = np.mgrid[-150:150:31j, 0:1, -150:150:31j].squeeze().transpose((1, 2, 0)) | ||
| ez = np.zeros_like(grid[..., 0]) | ||
| valid = chain.valid_points(grid, radii) | ||
| vals = [] | ||
| for i, r in enumerate(grid[valid]): | ||
| swb = treams.SphericalWaveBasis.default(1, positions=[r]) | ||
| field = sca.expandlattice(basis=swb).efield(r) | ||
| vals.append(np.real(inc.efield(r)[2] + field[2])) | ||
| ez[valid] = vals | ||
| ez[~valid] = np.nan | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 0], ez.T, shading="nearest", vmin=-0.5, vmax=0.5, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("$E_z$") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0 = 2 * np.pi / 700 | ||
| materials = [treams.Material(-16.5 + 1j), treams.Material()] | ||
| lmax = mmax = 3 | ||
| radii = [65, 55] | ||
| positions = [[-40, -50, 0], [40, 50, 0]] | ||
| lattice = treams.Lattice(300) | ||
| kz = 0.005 | ||
| sphere = treams.TMatrix.sphere(lmax, k0, radii[0], materials) | ||
| chain = sphere.latticeinteraction.solve(lattice, kz) | ||
| bmax = 3.1 * lattice.reciprocal | ||
| cwb = treams.CylindricalWaveBasis.diffr_orders(kz, mmax, lattice, bmax) | ||
| chain_tmc = treams.TMatrixC.from_array(chain, cwb) | ||
| cylinder = treams.TMatrixC.cylinder(np.unique(cwb.kz), mmax, k0, radii[1], materials) | ||
| cluster = treams.TMatrixC.cluster([chain_tmc, cylinder], positions).interaction.solve() | ||
| inc = treams.plane_wave( | ||
| [np.sqrt(k0 ** 2 - kz ** 2), 0, kz], | ||
| [np.sqrt(0.5), np.sqrt(0.5)], | ||
| k0=chain.k0, | ||
| material=chain.material, | ||
| ) | ||
| sca = cluster @ inc.expand(cluster.basis) | ||
| grid = ( | ||
| np.mgrid[-300:300:61j, 0:1, -150:150:31j].squeeze().transpose((1, 2, 0))[:, :-1, :] | ||
| ) | ||
| ez = np.zeros_like(grid[..., 0], complex) | ||
| valid = cluster.valid_points(grid, radii) | ||
| ez[valid] = (inc.efield(grid[valid]) + sca.efield(grid[valid]))[..., 2] | ||
| ez = np.concatenate( | ||
| ( | ||
| np.real(ez.T * np.exp(-1j * kz * lattice[...])), | ||
| ez.T.real, | ||
| np.real(ez.T * np.exp(1j * kz * lattice[...])), | ||
| ) | ||
| ) | ||
| zaxis = np.concatenate((grid[0, :, 2] - 300, grid[0, :, 2], grid[0, :, 2] + 300,)) | ||
| fig, ax = plt.subplots(figsize=(10, 20)) | ||
| pcm = ax.pcolormesh(grid[:, 0, 0], zaxis, ez, shading="nearest", vmin=-1, vmax=1,) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("$E_z$") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| ax.set_aspect("equal") | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0 = 2 * np.pi / 1000 | ||
| materials = [treams.Material(16 + 0.5j), treams.Material()] | ||
| lmax = 3 | ||
| radii = [110, 90, 80, 75] | ||
| positions = (220 / np.sqrt(24)) * np.array( | ||
| [ | ||
| [np.sqrt(8), 0, -1], | ||
| [-np.sqrt(2), np.sqrt(6), -1], | ||
| [-np.sqrt(2), -np.sqrt(6), -1], | ||
| [0, 0, 3], | ||
| ] | ||
| ) | ||
| spheres = [treams.TMatrix.sphere(lmax, k0, r, materials) for r in radii] | ||
| tm = treams.TMatrix.cluster(spheres, positions).interaction.solve() | ||
| inc = treams.plane_wave([k0, 0, 0], 0, k0=tm.k0, material=tm.material) | ||
| sca = tm @ inc.expand(tm.basis) | ||
| xs = tm.xs(inc) | ||
| grid = np.mgrid[-300:300:101j, 0:1, -300:300:101j].squeeze().transpose((1, 2, 0)) | ||
| intensity = np.zeros_like(grid[..., 0]) | ||
| valid = tm.valid_points(grid, radii) | ||
| intensity[~valid] = np.nan | ||
| intensity[valid] = 0.5 * np.sum( | ||
| np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1 | ||
| ) | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 0], intensity.T, shading="nearest", vmin=0, vmax=2, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("Intensity") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| ax.text( | ||
| 0, | ||
| 0, | ||
| r"$\sigma_\mathrm{sca} = " | ||
| f"{xs[0]:.7}\\,$nm$^2$\n" | ||
| r"$\sigma_\mathrm{ext} = " | ||
| f"{xs[1]:.7}\\,$nm$^2$", | ||
| ) | ||
| fig.show() | ||
| tm_global = tm.expand(treams.SphericalWaveBasis.default(6)) | ||
| sca = tm_global @ inc.expand(tm_global.basis) | ||
| xs = tm_global.xs(inc) | ||
| grid = np.mgrid[-300:300:101j, 0:1, -300:300:101j].squeeze().transpose((1, 2, 0)) | ||
| intensity_global = np.zeros_like(grid[..., 0]) | ||
| valid = tm_global.valid_points(grid, [260]) | ||
| intensity_global[~valid] = np.nan | ||
| intensity_global[valid] = 0.5 * np.sum( | ||
| np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1 | ||
| ) | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 0], intensity_global.T, shading="nearest", vmin=0, vmax=2, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("Intensity") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| ax.text( | ||
| 0, | ||
| 0, | ||
| r"$\sigma_\mathrm{sca} = " | ||
| f"{xs[0]:.7}\\,$nm$^2$\n" | ||
| r"$\sigma_\mathrm{ext} = " | ||
| f"{xs[1]:.7}\\,$nm$^2$", | ||
| ) | ||
| fig.show() | ||
| inc = treams.plane_wave([0, 0, -k0], 0, k0=tm.k0, material=tm.material) | ||
| tm_rotate = tm_global.rotate(0, np.pi / 2) | ||
| sca = tm_rotate @ inc.expand(tm_rotate.basis) | ||
| xs = tm_rotate.xs(inc) | ||
| grid = np.mgrid[-300:300:101j, 0:1, -300:300:101j].squeeze().transpose((1, 2, 0)) | ||
| intensity_global = np.zeros_like(grid[..., 0]) | ||
| valid = tm_rotate.valid_points(grid, [260]) | ||
| intensity_global[~valid] = np.nan | ||
| intensity_global[valid] = 0.5 * np.sum( | ||
| np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1 | ||
| ) | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 0], intensity_global.T, shading="nearest", vmin=0, vmax=2, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("Intensity") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| ax.text( | ||
| 0, | ||
| 0, | ||
| r"$\sigma_\mathrm{sca} = " | ||
| f"{xs[0]:.7}\\,$nm$^2$\n" | ||
| r"$\sigma_\mathrm{ext} = " | ||
| f"{xs[1]:.7}\\,$nm$^2$", | ||
| ) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(0.01, 1, 200) | ||
| materials = [treams.Material(12.4), treams.Material()] | ||
| mmax = 3 | ||
| radius = 0.2 | ||
| lattice = treams.Lattice.square(0.3) | ||
| kpar = [0, 0, 0] | ||
| res = [] | ||
| for k0 in k0s: | ||
| cyl = treams.TMatrixC.cylinder(kpar[2], mmax, k0, radius, materials) | ||
| svd = np.linalg.svd(cyl.latticeinteraction(lattice, kpar[:2]), compute_uv=False) | ||
| res.append(svd[-1]) | ||
| fig, ax = plt.subplots() | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.set_ylabel("Smallest singular value") | ||
| ax.semilogy(299.792458 * k0s / (2 * np.pi), res) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(0.01, 1, 200) | ||
| materials = [treams.Material(12.4), treams.Material()] | ||
| lmax = 3 | ||
| radius = 0.2 | ||
| lattice = treams.Lattice.cubic(0.5) | ||
| kpar = [0, 0, 0] | ||
| res = [] | ||
| for k0 in k0s: | ||
| sphere = treams.TMatrix.sphere(lmax, k0, radius, materials) | ||
| svd = np.linalg.svd(sphere.latticeinteraction(lattice, kpar), compute_uv=False) | ||
| res.append(svd[-1]) | ||
| fig, ax = plt.subplots() | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.set_ylabel("Smallest singular value") | ||
| ax.semilogy(299.792458 * k0s / (2 * np.pi), res) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(1 / 6000, 1 / 300, 200) | ||
| materials = [treams.Material(16 + 0.5j), treams.Material()] | ||
| mmax = 4 | ||
| radius = 75 | ||
| kzs = [0] | ||
| cylinders = [treams.TMatrixC.cylinder(kzs, mmax, k0, radius, materials) for k0 in k0s] | ||
| xw_sca = np.array([tm.xw_sca_avg for tm in cylinders]) / (2 * radius) | ||
| xw_ext = np.array([tm.xw_ext_avg for tm in cylinders]) / (2 * radius) | ||
| cwb_mmax0 = treams.CylindricalWaveBasis.default(kzs, 0) | ||
| cylinders_mmax0 = [tm[cwb_mmax0] for tm in cylinders] | ||
| xw_sca_mmax0 = np.array([tm.xw_sca_avg for tm in cylinders_mmax0]) / (2 * radius) | ||
| xw_ext_mmax0 = np.array([tm.xw_ext_avg for tm in cylinders_mmax0]) / (2 * radius) | ||
| tm = cylinders[-1] | ||
| inc = treams.plane_wave([tm.k0, 0, 0], 1, k0=tm.k0, material=tm.material) | ||
| sca = tm @ inc.expand(tm.basis) | ||
| grid = np.mgrid[-100:100:101j, -100:100:101j, 0:1].squeeze().transpose((1, 2, 0)) | ||
| intensity = np.zeros_like(grid[..., 0]) | ||
| valid = tm.valid_points(grid, [radius]) | ||
| intensity[~valid] = np.nan | ||
| intensity[valid] = 0.5 * np.sum( | ||
| np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1 | ||
| ) | ||
| fig, ax = plt.subplots() | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xw_ext) | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xw_sca) | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xw_ext_mmax0, color="C0", linestyle=":") | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xw_sca_mmax0, color="C1", linestyle=":") | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.set_ylabel("Efficiency") | ||
| ax.legend(["Extinction", "Scattering", "Extinction $m=1$", "Scattering $m=1$"]) | ||
| fig.show() | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 1], grid[:, 0, 0], intensity.T, shading="nearest", vmin=0, vmax=1, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("Intensity") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("y (nm)") | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0 = 2 * np.pi / 700 | ||
| materials = [treams.Material(-16.5 + 1j), treams.Material()] | ||
| lmax = mmax = 3 | ||
| radii = [65, 55] | ||
| positions = [[-40, -50, 0], [40, 50, 0]] | ||
| lattice_z = treams.Lattice(300) | ||
| lattice_x = treams.Lattice(300, "x") | ||
| kz = 0.005 | ||
| sphere = treams.TMatrix.sphere(lmax, k0, radii[0], materials) | ||
| chain = sphere.latticeinteraction.solve(lattice_z, kz) | ||
| bmax = 1.1 * lattice_z.reciprocal | ||
| cwb = treams.CylindricalWaveBasis.diffr_orders(kz, mmax, lattice_z, bmax) | ||
| kzs = np.unique(cwb.kz) | ||
| chain_tmc = treams.TMatrixC.from_array(chain, cwb) | ||
| cylinder = treams.TMatrixC.cylinder(kzs, mmax, k0, radii[1], materials) | ||
| cluster = treams.TMatrixC.cluster( | ||
| [chain_tmc, cylinder], positions | ||
| ).latticeinteraction.solve(lattice_x, 0) | ||
| inc = treams.plane_wave( | ||
| [0, np.sqrt(k0 ** 2 - kz ** 2), kz], | ||
| [np.sqrt(0.5), -np.sqrt(0.5)], | ||
| k0=chain.k0, | ||
| material=chain.material, | ||
| ) | ||
| sca = cluster @ inc.expand(cluster.basis) | ||
| grid = np.mgrid[0:1, -150:150:16j, -150:150:16j].squeeze().transpose((1, 2, 0)) | ||
| ex = np.zeros_like(grid[..., 0]) | ||
| valid = cluster.valid_points(grid, radii) | ||
| vals = [] | ||
| for i, r in enumerate(grid[valid]): | ||
| cwb = treams.CylindricalWaveBasis.default(kzs, 1, positions=[r]) | ||
| field = sca.expandlattice(basis=cwb).efield(r) | ||
| vals.append(np.real(inc.efield(r)[0] + field[0])) | ||
| ex[valid] = vals | ||
| ex[~valid] = np.nan | ||
| fig, ax = plt.subplots(figsize=(10, 20)) | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 1], ex.T, shading="nearest", vmin=-1, vmax=1, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("$E_x$") | ||
| ax.set_xlabel("y (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| ax.set_aspect("equal") | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0 = 2 * np.pi / 700 | ||
| materials = [treams.Material(-16.5 + 1j), treams.Material()] | ||
| lmax = 3 | ||
| radii = [75, 75] | ||
| positions = [[-75, 0, 30], [75, 0, -30]] | ||
| lattice = treams.Lattice.square(300) | ||
| kpar = [0, 0] | ||
| spheres = [treams.TMatrix.sphere(lmax, k0, r, materials) for r in radii] | ||
| array = treams.TMatrix.cluster(spheres, positions).latticeinteraction.solve( | ||
| lattice, kpar | ||
| ) | ||
| inc = treams.plane_wave([0, 0, k0], [-1, 0, 0], k0=array.k0, material=array.material) | ||
| sca = array @ inc.expand(array.basis) | ||
| grid = np.mgrid[-150:150:31j, 0:1, -150:150:31j].squeeze().transpose((1, 2, 0)) | ||
| ez = np.zeros_like(grid[..., 0]) | ||
| valid = array.valid_points(grid, radii) | ||
| vals = [] | ||
| for i, r in enumerate(grid[valid]): | ||
| swb = treams.SphericalWaveBasis.default(1, positions=[r]) | ||
| field = sca.expandlattice(basis=swb).efield(r) | ||
| vals.append(np.real(inc.efield(r)[2] + field[2])) | ||
| ez[valid] = vals | ||
| ez[~valid] = np.nan | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 0], ez.T, shading="nearest", vmin=-1, vmax=1, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("$E_z$") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(1 / 1000, 1 / 300, 50) | ||
| materials = [(), (12.4 + 1j, 1 + 0.1j, 0.5 + 0.05j), (2, 2)] | ||
| thickness = 50 | ||
| tr = np.zeros((2, len(k0s), 2)) | ||
| for i, k0 in enumerate(k0s): | ||
| pwb = treams.PlaneWaveBasisByComp.default([0, 0.5 * k0]) | ||
| slab = treams.SMatrices.slab(thickness, pwb, k0, materials) | ||
| tr[0, i, :] = slab.tr([1, 0]) | ||
| tr[1, i, :] = slab.tr([0, 1]) | ||
| fig, ax = plt.subplots() | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[0, :, 0]) | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[0, :, 1]) | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[1, :, 0], c="C0", linestyle="--") | ||
| ax.plot(299792.458 * k0s / (2 * np.pi), tr[1, :, 1], c="C1", linestyle="--") | ||
| ax.legend(["$T_+$", "$R_+$", "$T_-$", "$R_-$"]) | ||
| fig.show() |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import treams | ||
| k0s = 2 * np.pi * np.linspace(1 / 700, 1 / 300, 200) | ||
| materials = [treams.Material(16 + 0.5j), treams.Material()] | ||
| lmax = 4 | ||
| radius = 75 | ||
| spheres = [treams.TMatrix.sphere(lmax, k0, radius, materials) for k0 in k0s] | ||
| xs_sca = np.array([tm.xs_sca_avg for tm in spheres]) / (np.pi * radius ** 2) | ||
| xs_ext = np.array([tm.xs_ext_avg for tm in spheres]) / (np.pi * radius ** 2) | ||
| swb_lmax1 = treams.SphericalWaveBasis.default(1) | ||
| spheres_lmax1 = [tm[swb_lmax1] for tm in spheres] | ||
| xs_sca_lmax1 = np.array([tm.xs_sca_avg for tm in spheres_lmax1]) / (np.pi * radius ** 2) | ||
| xs_ext_lmax1 = np.array([tm.xs_ext_avg for tm in spheres_lmax1]) / (np.pi * radius ** 2) | ||
| tm = spheres[-1] | ||
| inc = treams.plane_wave([0, 0, tm.k0], 1, k0=tm.k0, material=tm.material) | ||
| sca = tm @ inc.expand(tm.basis) | ||
| grid = np.mgrid[-100:100:101j, 0:1, -100:100:101j].squeeze().transpose((1, 2, 0)) | ||
| intensity = np.zeros_like(grid[..., 0]) | ||
| valid = tm.valid_points(grid, [radius]) | ||
| intensity[~valid] = np.nan | ||
| intensity[valid] = 0.5 * np.sum( | ||
| np.abs(inc.efield(grid[valid]) + sca.efield(grid[valid])) ** 2, -1 | ||
| ) | ||
| fig, ax = plt.subplots() | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xs_ext) | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xs_sca) | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xs_ext_lmax1, color="C0", linestyle=":") | ||
| ax.plot(k0s * 299792.458 / (2 * np.pi), xs_sca_lmax1, color="C1", linestyle=":") | ||
| ax.set_xlabel("Frequency (THz)") | ||
| ax.set_ylabel("Efficiency") | ||
| ax.legend(["Extinction", "Scattering", "Extinction $l=1$", "Scattering $l=1$"]) | ||
| fig.show() | ||
| fig, ax = plt.subplots() | ||
| pcm = ax.pcolormesh( | ||
| grid[0, :, 2], grid[:, 0, 0], intensity.T, shading="nearest", vmin=0, vmax=1, | ||
| ) | ||
| cb = plt.colorbar(pcm) | ||
| cb.set_label("Intensity") | ||
| ax.set_xlabel("x (nm)") | ||
| ax.set_ylabel("z (nm)") | ||
| fig.show() |
| .. highlight:: console | ||
| =============== | ||
| Getting started | ||
| =============== | ||
| Installation | ||
| ============ | ||
| To install the package with pip, use :: | ||
| pip install treams | ||
| How to use treams | ||
| ================= | ||
| Import *treams*, create T-matrices and start calculating. | ||
| .. doctest:: | ||
| >>> import treams | ||
| >>> tm = treams.TMatrix.sphere(1, .6, 1, [4, 1]) | ||
| >>> tm.xs_ext_avg | ||
| 0.3072497765576123 | ||
| More detailed examples are given in :doc:`intro`. |
| ================================= | ||
| Welcome to treams' documentation! | ||
| ================================= | ||
| .. toctree:: | ||
| :maxdepth: 1 | ||
| gettingstarted | ||
| intro | ||
| theory | ||
| treams | ||
| dev | ||
| about | ||
| The package **treams** provides a framework to simplify computations of the | ||
| electromagnetic scattering of waves at finite and periodically infinite arrangements of | ||
| particles. All methods are suitable for the use of chiral materials. The periodic | ||
| systems can have one-, two-, or three-dimensional lattices. The lattice computations are | ||
| accelerated by converting the occurring slowly converging summations to exponentially | ||
| fast convergent series. | ||
| To accommodate the periodic structures of different dimensionalities, three types of | ||
| solutions to the vectorial Helmholtz equation are employed: plane waves, cylindrical | ||
| waves, and spherical waves. For each of those solution sets, the typical manipulations, | ||
| e.g. translations and rotations, are implemented, as well as transformations between | ||
| them. | ||
| The package contains two subpackages: :mod:`lattice` and :mod:`special`. The former | ||
| contains mainly the functions for computing the lattice series. The latter can be seen | ||
| as an addition to the special functions implemented in :py:mod:`scipy.special`. It | ||
| contains the mathematical functions that are typically necessary in T-Matrix method | ||
| computations. | ||
| Finally, three classes are the main point of interaction for the user. They allow access | ||
| to the underlying functions operating directly on the spherical and cylindrical | ||
| T-matrices or the S-matrices based on the plane wave solutions. | ||
| .. todo:: clean up intro | ||
| Features | ||
| ======== | ||
| * T-matrix calculations using a spherical or cylindrical wave basis set | ||
| * Calculations in helicity and parity (TE/TM) basis | ||
| * Scattering from clusters of particles | ||
| * Scattering from particles and clusters arranged in 3d-, 2d-, and 1d-lattices | ||
| * Calculation of light propagation in stratified media | ||
| * Band calculation in crystal structures | ||
| Indices and tables | ||
| ================== | ||
| * :ref:`genindex` | ||
| * :ref:`modindex` | ||
| * :ref:`search` |
| ======== | ||
| Examples | ||
| ======== | ||
| *treams* is a program that covers various aspects of T-matrix calculations and | ||
| associated topics. The functionality can be roughly separated into three levels: | ||
| low-level functions, intermediate-level functions, and high-level functions and classes. | ||
| The low-level functions implement the underlying mathematical functions, that build | ||
| the foundation of T-matrix calculations. They are mainly located in the two subpackages | ||
| :mod:`treams.special` and :mod:`treams.lattice`. The first one contains, e.g., the | ||
| various solutions to the Helmholtz equation and their translation coefficients, the | ||
| second subpackage contains functions that are associated with computations in lattices. | ||
| On the intermediate-level those underlying functions are combined to provide functions | ||
| as they are often needed for T-matrix calculations, e.g., the Mie coefficients or the | ||
| expansion coefficients of vector plane waves into spherical waves. The low- and | ||
| intermediate-level functions are mostly focused on speed, they are usually implemented | ||
| in compiled code and are often vectorized. The latter aspect also helps with the | ||
| integration into the framework provided by numpy. | ||
| The high-level functionality is more focused on the usability. We attempt to create an | ||
| useful interface to the underlying functions, that reduces redundancy and that is less | ||
| error prone than using the pure functions, while still integrating nicely with numpy | ||
| functions. It consists of a combination of different classes and functions. At first, | ||
| there are the different basis sets, that can be used together with other important | ||
| parameters for the calculation, for example the embedding materials or the lattice | ||
| definitions. Then, there are the "physics-aware arrays", which keep track of these | ||
| parameters during the computation, and operators that can be applied to these arrays. | ||
| Finally, we will introduce, how these previous concepts can be applied to T-Matrices for | ||
| vector spherical and cylindrical solutions and to S-Matrices for vector plane wave | ||
| solutions. | ||
| .. toctree:: | ||
| :maxdepth: 1 | ||
| params | ||
| physicsarray | ||
| operators | ||
| tmatrix | ||
| tmatrixc | ||
| smatrix |
| @ECHO OFF | ||
| pushd %~dp0 | ||
| REM Command file for Sphinx documentation | ||
| if "%SPHINXBUILD%" == "" ( | ||
| set SPHINXBUILD=sphinx-build | ||
| ) | ||
| set SOURCEDIR=. | ||
| set BUILDDIR=_build | ||
| if "%1" == "" goto help | ||
| %SPHINXBUILD% >NUL 2>NUL | ||
| if errorlevel 9009 ( | ||
| echo. | ||
| echo.The 'sphinx-build' command was not found. Make sure you have Sphinx | ||
| echo.installed, then set the SPHINXBUILD environment variable to point | ||
| echo.to the full path of the 'sphinx-build' executable. Alternatively you | ||
| echo.may add the Sphinx directory to PATH. | ||
| echo. | ||
| echo.If you don't have Sphinx installed, grab it from | ||
| echo.http://sphinx-doc.org/ | ||
| exit /b 1 | ||
| ) | ||
| %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% | ||
| goto end | ||
| :help | ||
| %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% | ||
| :end | ||
| popd |
| # Minimal makefile for Sphinx documentation | ||
| # | ||
| # You can set these variables from the command line, and also | ||
| # from the environment for the first two. | ||
| SPHINXOPTS ?= | ||
| SPHINXBUILD ?= sphinx-build | ||
| SOURCEDIR = . | ||
| BUILDDIR = _build | ||
| # Put it first so that "make" without argument is like "make help". | ||
| help: | ||
| @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) | ||
| .PHONY: help Makefile | ||
| # Catch-all target: route all unknown targets to Sphinx using the new | ||
| # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). | ||
| %: Makefile | ||
| @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
-401
| ===================================================== | ||
| Maxwell's equations and chiral constitutive relations | ||
| ===================================================== | ||
| In matter Maxwell's equations can be written in frequency domain in the absence of free | ||
| charges and currents as | ||
| .. math:: | ||
| \nabla \cdot | ||
| \begin{pmatrix} | ||
| \frac{1}{\epsilon_0} \boldsymbol D \\ | ||
| c \boldsymbol B | ||
| \end{pmatrix} | ||
| = 0 | ||
| and | ||
| .. math:: | ||
| \nabla \times | ||
| \begin{pmatrix} | ||
| \boldsymbol E \\ | ||
| Z_0 \boldsymbol H | ||
| \end{pmatrix} | ||
| = | ||
| k_0 | ||
| \begin{pmatrix} | ||
| 0 & \mathrm i \\ | ||
| - \mathrm i & 0 | ||
| \end{pmatrix} | ||
| \begin{pmatrix} | ||
| \frac{1}{\epsilon_0} \boldsymbol D \\ | ||
| c \boldsymbol B | ||
| \end{pmatrix} | ||
| where :math:`\boldsymbol E`, :math:`\boldsymbol H`, :math:`\boldsymbol D`, and | ||
| :math:`\boldsymbol B` are the electric and magnetic fields, the displacement field, and | ||
| the magnetic flux density (:func:`treams.efield`, :func:`treams.hfield`, | ||
| :func:`treams.dfield`, :func:`treams.bfield`). All these quantities are complex valued | ||
| fields, that depend on the angular frequency :math:`\omega` and the position | ||
| :math:`\boldsymbol r`, which we omitted here for a conciser notation. The speed of light | ||
| (in vacuum) :math:`c`, the free space impedance :math:`Z_0`, and the vacuum permittivity | ||
| :math:`\epsilon_0` are chosen as constant prefactors such that all fields are normalized | ||
| to the same units. Conventionally, within treams the (vacuum) wave number | ||
| :math:`k_0 = \frac{\omega}{c}` is generally used to express the frequency. | ||
| For the transformation to the time domain we use for a general function | ||
| :math:`f(\omega)` | ||
| .. math:: | ||
| f(t) = \int_{-\infty}^\infty \mathrm d t f(\omega) \mathrm e^{-\mathrm i \omega t} | ||
| as Fourier transformation convention, and thus the inverse transformation is | ||
| .. math:: | ||
| f(\omega) | ||
| = \int_{-\infty}^\infty \frac{\mathrm d \omega}{2 \pi} | ||
| f(t) \mathrm e^{\mathrm i \omega t} | ||
| To solve those equations they have to be complemented with constitutive relations. In a | ||
| linear, time-invariant, homogeneous, isotropic, local and reciprocal medium the relation | ||
| of the four electromagnetic fields can be expressed by | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| \frac{1}{\epsilon_0} \boldsymbol D \\ | ||
| c \boldsymbol B | ||
| \end{pmatrix} | ||
| = | ||
| \begin{pmatrix} | ||
| \epsilon & \mathrm i \kappa \\ | ||
| - \mathrm i \kappa & \mu | ||
| \end{pmatrix} | ||
| \begin{pmatrix} | ||
| \boldsymbol E \\ | ||
| Z_0 \boldsymbol H | ||
| \end{pmatrix} | ||
| where :math:`\epsilon`, :math:`\mu`, and :math:`\kappa` are the relative permittivity, | ||
| relative permeability, and chirality parameter (:class:`treams.Material`). Due to the | ||
| requirement of isotropy these quantities are all scalar. | ||
| The combination of the curl equation and the constitutive relations leads to the | ||
| equation | ||
| .. math:: | ||
| \nabla \times | ||
| \begin{pmatrix} | ||
| \boldsymbol E \\ | ||
| Z_0 \boldsymbol H | ||
| \end{pmatrix} | ||
| = | ||
| k_0 | ||
| \begin{pmatrix} | ||
| \kappa & \mathrm i \mu \\ | ||
| - \mathrm i \epsilon & \kappa | ||
| \end{pmatrix} | ||
| \begin{pmatrix} | ||
| \boldsymbol E \\ | ||
| Z_0 \boldsymbol H | ||
| \end{pmatrix} | ||
| that can be diagonalized to yield | ||
| .. math:: | ||
| \nabla \times | ||
| \begin{pmatrix} | ||
| \boldsymbol G_- \\ | ||
| \boldsymbol G_+ | ||
| \end{pmatrix} | ||
| = | ||
| \begin{pmatrix} | ||
| -k_- & 0 \\ | ||
| 0 & k_+ | ||
| \end{pmatrix} | ||
| \begin{pmatrix} | ||
| \boldsymbol G_- \\ | ||
| \boldsymbol G_+ | ||
| \end{pmatrix} | ||
| where the Riemann-Silberstein vectors :math:`\sqrt{2} \boldsymbol G_\pm = \boldsymbol E | ||
| \pm \mathrm i Z_0 Z \boldsymbol H` appear (:func:`treams.gfield`), with the relative | ||
| impedance defined as :math:`Z = \sqrt{\frac{\mu}{\epsilon}}`. The wave numbers in the | ||
| medium are :math:`k_\pm = k_0 n_pm = k_0 (n \pm \kappa)` with the refractive index | ||
| :math:`n = \sqrt{\epsilon \mu}`. | ||
| The alternative definition of the Riemann-Silberstein vectors :math:`\sqrt{2} | ||
| \boldsymbol F_\pm = \frac{1}{\epsilon_0 \epsilon} \boldsymbol D \pm \mathrm i | ||
| \frac{c}{n} \boldsymbol B` using the displacement field and the magnetic flux density | ||
| instead of the electric and magnetic field is related to the definition above by | ||
| :math:`\boldsymbol F_\pm = \frac{n_\pm}{n} \boldsymbol G_\pm` (:func:`treams.ffield`). | ||
| In isotropic media the divergence equations simply become :math:`\nabla | ||
| \boldsymbol G_\pm = 0 = \nabla \boldsymbol F_\pm`. | ||
| Solutions to the vector Helmholtz equation | ||
| ========================================== | ||
| Instead of immediatly solving Maxwell's equations from above, we will study the | ||
| Helmholtz equation which is commonly encountered when studying wave phenomena first. | ||
| This section mainly relies on [#]_. | ||
| The vector Helmholtz equation is | ||
| .. math:: | ||
| (\Delta + k^2) \boldsymbol f | ||
| = | ||
| \nabla (\nabla \boldsymbol f) | ||
| - \nabla \times \nabla \times \boldsymbol f | ||
| + k^2 \boldsymbol f | ||
| = 0 | ||
| where :math:`\Delta` is the Laplace operator. Note, that by applying the curl operator | ||
| twice on the Riemann-Silberstein vectors (in the case of an achiral material this is | ||
| also true for the electric and magnetic fields) and using the transversality condition | ||
| for the fields, the vector Helmholtz equation can be easily obtained. | ||
| Solutions to the vector Helmholtz equation can be obtained from solutions to the scalar | ||
| Helmholtz equation :math:`(\Delta + k^2) f = \nabla (\nabla f) - k^2 f = 0` by using the | ||
| construction | ||
| .. math:: | ||
| \boldsymbol L = \boldsymbol v f \\ | ||
| \boldsymbol M = \nabla \times (\boldsymbol v f) \\ | ||
| \boldsymbol N = \nabla \times \nabla \times (\boldsymbol v f) | ||
| where :math:`\boldsymbol v` is a steering vector that depends on the coordinate system | ||
| used for the solution :math:`f`. We will focus the following discussion on the three | ||
| cases of planar, cylindrical, and spherical solutions, where the coordinate systems are | ||
| chosen to be Cartesian, cylindrical, and spherical. Also, we will limit the discussion | ||
| of the first type of solution, because it is not transverse. | ||
| Plane waves | ||
| ----------- | ||
| In Cartesian coordinates the solution to the scalar Helmholtz equation are simple | ||
| plane waves :math:`\mathrm e^{\mathrm i \boldsymbol k \boldsymbol r}` where the wave | ||
| vector fulfils :math:`\boldsymbol k^2 = k_x^2 + k_y^2 + k_z^2 = k^2`. The steering | ||
| vector is constant and conventionally chosen to be the unit vector along the z-axis | ||
| :math:`\boldsymbol{\hat z}`. Then, the solutions | ||
| .. math:: | ||
| \boldsymbol M_{\boldsymbol k} (k, \boldsymbol r) | ||
| = | ||
| \mathrm i | ||
| \frac{k_y \boldsymbol{\hat x} - k_x \boldsymbol{\hat y}}{\sqrt{k_x^2 + k_y^2}} | ||
| \mathrm e^{\mathrm i \boldsymbol k \boldsymbol r} | ||
| = | ||
| -\mathrm i \boldsymbol{\hat \varphi}_{\boldsymbol k} | ||
| \mathrm e^{\mathrm i \boldsymbol k \boldsymbol r} | ||
| \\ | ||
| \boldsymbol N_{\boldsymbol k} (k, \boldsymbol r) | ||
| = | ||
| \frac{-k_x k_z \boldsymbol{\hat x} - k_y k_z \boldsymbol{\hat y} + (k_x^2 + k_y^2) | ||
| \boldsymbol{\hat z}}{k\sqrt{k_x^2 + k_y^2}} | ||
| \mathrm e^{\mathrm i \boldsymbol k \boldsymbol r} | ||
| = -\boldsymbol{\hat \theta}_{\boldsymbol k} | ||
| \mathrm e^{\mathrm i \boldsymbol k \boldsymbol r} | ||
| are found (:func:`treams.special.vpw_M` and :func:`treams.special.vpw_N`). We normalized | ||
| these solutions such that they have unit strength for real-valued wave vectors. The | ||
| solution :math:`\boldsymbol M_{\boldsymbol k}` is always perpendicular to the z-axis. | ||
| Thus, with respect to the x-y-plane those solutions are often referred to as `TE`, when | ||
| taken for the electric field. Similarly, the solutions | ||
| :math:`\boldsymbol M_{\boldsymbol k}` are referred to as `TM`. | ||
| Cylindrical waves | ||
| ----------------- | ||
| The cylindrical solutions can be constructed mostly analogously to the plane waves. The | ||
| steering vector stays :math:`\boldsymbol{\hat z}`. The solutions in cylindrical | ||
| coordinates are :math:`Z_m^{(n)}(k_\rho \rho) \mathrm e^{\mathrm i (m \varphi + k_z z}` | ||
| where :math:`k_z \in \mathbb R` and :math:`m \in \mathbb Z` are the parameters of the | ||
| solution. The radial part of the wave vector is defined as :math:`k_\rho = | ||
| \sqrt{k^2 - k_z^2}` with the imaginary part of the square root to be taken non-negative. | ||
| The functions :math:`Z_m^{(n)}` are the Bessel and Hankel functions. For a complete set | ||
| of solutions it is necessary to select two of them. We generally use the (regular) | ||
| Bessel functions :math:`J_m = Z_m^{(1)}` and the Hankel functions of the first kind | ||
| :math:`H_m^{(1)} = Z_m^{(3)}` which are singular and correspond to radiating waves | ||
| (:func:`treams.special.jv`, :func:`treams.special.hankel1`). So, the cylindrical wave | ||
| solutions are | ||
| .. math:: | ||
| \boldsymbol M_{k_z, m}^{(n)} (k, \boldsymbol r) | ||
| = | ||
| \left(\frac{\mathrm i m}{k_\rho \rho} Z_m^{(n)}(k_\rho \rho) \boldsymbol{\hat \rho} | ||
| - {Z_m^{(n)}}'(k_\rho \rho) \boldsymbol{\hat \varphi}\right) | ||
| \mathrm e^{\mathrm i (m \varphi + k_z z)} | ||
| \\ | ||
| \boldsymbol N_{k_z, m}^{(n)} (k, \boldsymbol r) | ||
| = | ||
| \left(\frac{\mathrm i k_z}{k} {Z_m^{(1)}}'(k_\rho \rho) \boldsymbol{\hat \rho} | ||
| - \frac{m k_z}{k k_\rho \rho} Z_m^{(1)}(k_\rho \rho) \boldsymbol{\hat \varphi} | ||
| + \frac{k_\rho}{k} Z_m^{(1)}(k_\rho \rho) \boldsymbol{\hat z}\right) | ||
| \mathrm e^{\mathrm i (k_z z + m \varphi)} | ||
| where we, again, normalized the functions (:func:`treams.special.vcw_rM`, | ||
| :func:`treams.special.vcw_M`, :func:`treams.special.vcw_rN`, and | ||
| :func:`treams.special.vcw_N`). Since the steering vector is in the direction of the | ||
| z-axis, the solutions :math:`\boldsymbol M_{k_z, m}^{(n)}` lie always in the x-y-plane. | ||
| Spherical waves | ||
| --------------- | ||
| Finally, we define the spherical solutions starting from the scalar solutions | ||
| :math:`z_l^{(n)}(kr) Y_{lm}(\theta, \phi)` where :math:`z_l^{(n)}` are the spherical | ||
| Bessel and Hankel functions (and we choose :math:`j_l = z_l^{(1)}` and | ||
| :math:`h_l^{(1)} = z_l^{(n)}` in complete analogy to the cylindrical case) and | ||
| :math:`Y_{lm}` are the spherical harmonics (:func:`treams.special.spherical_jn`, | ||
| :func:`treams.special.spherical_hankel1`, and :func:`treams.special.sph_harm`). The | ||
| value :math:`l \in \mathbb N` refers to the angular momentum. The value :math:`l = 0` is | ||
| only possible for longitudinal modes. So, for electromagnetic waves generally | ||
| :math:`l \geq 1`. The projection of the angular momentum onto the z-axis is :math:`m \in | ||
| \mathbb Z` with :math:`|m| \leq l`. The steering vector for the spherical coordinate | ||
| solution is :math:`\boldsymbol r`. Then, the vector spherical waves are defined as | ||
| .. math:: | ||
| \boldsymbol M_{lm}^{(n)} (k, \boldsymbol r) | ||
| = z_l^{(n)} (kr) \boldsymbol X_{lm}(\theta, \varphi) | ||
| \\ | ||
| \boldsymbol N_{lm}^{(n)} (k, \boldsymbol r) | ||
| = | ||
| \left({h_l^{(1)}}'(kr) + \frac{h_l^{(1)}(kr)}{kr}\right) | ||
| \boldsymbol Y_{lm}(\theta, \varphi) | ||
| + \sqrt{l (l + 1)} \frac{h_l^{(1)}(kr)}{kr} \boldsymbol Z_{lm}(\theta, \varphi) | ||
| (:func:`treams.special.vsw_rM`, :func:`treams.special.vsw_M`, | ||
| :func:`treams.special.vsw_rN`, and :func:`treams.special.vsw_N`) where | ||
| .. math:: | ||
| \boldsymbol X_{lm} (\theta, \varphi) | ||
| = \mathrm i \sqrt{\frac{2 l + 1}{4 \pi l (l + 1)} \frac{(l - m)!}{(l + m)!}} | ||
| \left(\mathrm i \pi_l^m(\cos\theta) \boldsymbol{\hat\theta} | ||
| - \tau_l^m (\cos\theta) \boldsymbol{\hat\varphi}\right) | ||
| \mathrm e^{\mathrm i m \varphi} | ||
| \\ | ||
| \boldsymbol Y_{lm} (\theta, \varphi) | ||
| = \mathrm i \sqrt{\frac{2 l + 1}{4 \pi l (l + 1)} \frac{(l - m)!}{(l + m)!}} | ||
| \left(\tau_l^m (\cos\theta) \boldsymbol{\hat\theta} | ||
| + \mathrm i \pi_l^m (\cos\theta) \boldsymbol{\hat\varphi}\right) | ||
| \mathrm e^{\mathrm i m \varphi} | ||
| \\ | ||
| \boldsymbol Z_{lm} (\theta, \varphi) | ||
| = \mathrm i Y_{lm}(\theta, \varphi) \boldsymbol{\hat r} | ||
| are the vector spherical harmonics (:func:`treams.special.vsh_X`, | ||
| :func:`treams.special.vsh_Y`, and :func:`treams.special.vsh_Z`). These are themselves | ||
| defined by the functions :math:`\pi_l^m(x) = \frac{m P_l^m(x)}{\sqrt{1 - x^2}}`, | ||
| :math:`\tau_l^m(x) = \frac{\mathrm d}{\mathrm d \theta}P_l^m(x = \cos\theta)`, and | ||
| the associated Legendre polynomials :math:`P_l^m` (:func:`treams.special.pi_fun`, | ||
| :func:`treams.special.tau_fun`, and :func:`treams.special.lpmv`). The vector spherical | ||
| harmonics are orthogonal to each other and normalized to 1 upon integration over the | ||
| solid angle. | ||
| The solutions :math:`\boldsymbol M_{lm}^{(n)}` are transverse to a sphere due to the | ||
| steering vector pointing in the radial direction. They are referred to as `TE` but | ||
| -- confusingly -- also as `magnetic` because they correspond to the electric field of a | ||
| magnetic multipole. Conversely, the solutions :math:`\boldsymbol N_{lm}^{(n)}` are | ||
| called `TM` or `electric`. | ||
| Solutions to Maxwell's equations | ||
| ================================ | ||
| Up to now, we set up Maxwell's equations together with constitutive relations for chiral | ||
| media and found solutions to the vector Helmholtz equation. Next, we want to combine | ||
| those results. | ||
| Modes of well-defined helicity | ||
| ------------------------------ | ||
| First, we want to find solutions to the Riemann-Silberstein vectors | ||
| :math:`\boldsymbol G_\pm`. Although we can obtain the vector Helmholtz equation from | ||
| :math:`\nabla \times \boldsymbol G_\pm = \pm k_\pm \boldsymbol G_\pm`, we observe that | ||
| this equation is more restrictive, namely our solutions :math:`\boldsymbol M_\nu` and | ||
| :math:`\boldsymbol N_\nu`, where :math:`\nu` is just a placeholder for the actual | ||
| parameters that indexes the concrete set of solutions, are no solutions for it. However, | ||
| with the above definitions we find that :math:`\nabla \times | ||
| \boldsymbol M_\nu (k, \boldsymbol r) = k \boldsymbol N_\nu (k, \boldsymbol r)` and | ||
| :math:`\nabla \times \boldsymbol N_\nu(k, \boldsymbol r) = k \boldsymbol M_\nu | ||
| (k, \boldsymbol r)`. So, the combinations :math:`\sqrt{2} \boldsymbol A_{\pm,\nu} | ||
| (k, \boldsymbol r) = \boldsymbol N_\nu (k, \boldsymbol r) \pm \boldsymbol M_\nu | ||
| (k, \boldsymbol r)` are indeed solutions for the respective Riemann-Silberstein vectors | ||
| (:func:`treams.special.vpw_A`, :func:`treams.special.vcw_rA`, | ||
| :func:`treams.special.vcw_A`, :func:`treams.special.vsw_rA`, and | ||
| :func:`treams.special.vsw_A`). The solution for Maxwell's equations are then | ||
| .. math:: | ||
| \boldsymbol G_\pm(\boldsymbol r) | ||
| = \sqrt{2} \sum_\nu a_{\pm,\nu} \boldsymbol A_{\pm,\nu} (k_\pm, \boldsymbol r) | ||
| \\ | ||
| \boldsymbol F_\pm(\boldsymbol r) | ||
| = | ||
| \sqrt{2} \frac{n_\pm}{n} | ||
| \sum_\nu a_{\pm,\nu}\boldsymbol A_{\pm,\nu} (k_\pm, \boldsymbol r) | ||
| \\ | ||
| \boldsymbol E(\boldsymbol r) | ||
| = \sum_{s,\nu} a_{s,\nu} \boldsymbol A_{s,\nu} (k_s, \boldsymbol r) | ||
| \\ | ||
| Z_0 \boldsymbol H(\boldsymbol r) | ||
| = -\frac{\mathrm i}{Z} | ||
| \sum_{s,\nu} s a_{s,\nu} \boldsymbol A_{s,\nu} (k_s, \boldsymbol r) | ||
| \\ | ||
| \frac{1}{\epsilon_0} \boldsymbol D(\boldsymbol r) | ||
| = \frac{1}{Z} \sum_{s,\nu} n_s a_{s,\nu} \boldsymbol A_{s,\nu} (k_s, \boldsymbol r) | ||
| \\ | ||
| c \boldsymbol B(\boldsymbol r) | ||
| = -\mathrm i \sum_{s,\nu} s n_s a_{s,\nu} \boldsymbol A_{s,\nu} (k_s, \boldsymbol r) | ||
| and, because each of the individual modes is an eigenmode of the helicity operator | ||
| :math:`\frac{\nabla\times}{k}`, we call them `helicity` modes. Modes of well-defined | ||
| helicity are suitable solutions chiral media. | ||
| Parity modes | ||
| ------------ | ||
| When considering only achiral media, it is quite common to not use modes of well-defined | ||
| helicity but modes with well-defined parity, which are exactly the modes | ||
| :math:`\boldsymbol M_\nu` and :math:`\boldsymbol N_\nu` defined above. For achiral | ||
| materials, we have :math:`k_\pm = k` and by substituting :math:`\sqrt{2} a_{\pm,\nu} = | ||
| a_{N,\nu} \pm a_{M,\nu}` for the expansion coefficients we find the solutions | ||
| .. math:: | ||
| \boldsymbol E(\boldsymbol r) | ||
| = \sum_\nu (a_{M,\nu} \boldsymbol M_\nu (k, \boldsymbol r) | ||
| + a_{N,\nu} \boldsymbol N_\nu (k, \boldsymbol r)) | ||
| \\ | ||
| Z_0 \boldsymbol H(\boldsymbol r) | ||
| = -\frac{\mathrm i}{Z} | ||
| \sum_\nu (a_{N,\nu} \boldsymbol M_\nu (k, \boldsymbol r) | ||
| + a_{M,\nu} \boldsymbol N_\nu (k, \boldsymbol r)) | ||
| \\ | ||
| \frac{1}{\epsilon_0} \boldsymbol D(\boldsymbol r) | ||
| = \epsilon \sum_\nu (a_{M,\nu} \boldsymbol M_\nu (k, \boldsymbol r) | ||
| + a_{N,\nu} \boldsymbol N_\nu (k, \boldsymbol r)) | ||
| \\ | ||
| c \boldsymbol B(\boldsymbol r) | ||
| = -\mathrm i n \sum_\nu (a_{N,\nu} \boldsymbol M_\nu (k, \boldsymbol r) | ||
| + a_{M,\nu} \boldsymbol N_\nu (k, \boldsymbol r)) | ||
| for the parity modes. | ||
| References | ||
| ========== | ||
| .. [#] P. M. Morse and H. Feshbach, Methods of Theoretical Physics | ||
| (McGraw-Hill, New York, 1953). |
| ========= | ||
| Operators | ||
| ========= | ||
| There are numerous operators implemented in *treams*. They replicate to some extend the | ||
| way active transformations with operators work on linear mapping :math:`M` | ||
| .. math:: | ||
| O(x) M O^{-1}(x) | ||
| where :math:`O` is the transformation we want to apply and and :math:`x` is a | ||
| parameter of that transformation. Similarly, | ||
| .. math:: | ||
| O(x) \psi | ||
| transforms state :math:`\psi` that can be represented by a vector. We attempt to | ||
| replicate this transformation notation in code and to extend it by other useful | ||
| functions. Later, the state :math:`\psi` can often be the expansion coefficients of | ||
| a wave, the linear mapping could be the T-matrix and the transformation operator could | ||
| be a rotation. | ||
| Each operator is implemented as a class. The class is instantiated with the parameter | ||
| :code:`op = Operator(x)`. At this stage it is only an abstract operator. A concrete | ||
| representation can be obtained by calling the operator with the necessary keyword | ||
| arguments, e.g. :code:`op(basis=concrete_basis)`, which will return a array-like | ||
| structure. | ||
| However, to be able to replicate the above mathematical notation it is also possible to | ||
| use the matrix multiplication operator between an array and the operator. The array | ||
| needs to have the necessary keywords as attributes, this is for what :doc:`physicsarray` | ||
| come in handy. For such an array :code:`arr` it is possible to type :code:`op @ arr` or | ||
| :code:`op @ arr @ op.inv`. The inverse of operators is implemented for many operators | ||
| but some have no inverse defined. | ||
| Sometimes, it can come in handy to directly apply an operator to an array without | ||
| defining the abstract operator before. This can be achieved by | ||
| :code:`arr.operator.apply_left(x)` and :code:`arr.operator.apply_right(x)`, which are | ||
| equivalent to :code:`op @ arr` and :code:`arr @ op.inv`, respectively. The function | ||
| :code:`arr.op()` is also defined. For arrays with ``ndim`` equal to 1 or without an | ||
| inverse, it is equivalent to :code:`arr.op.apply_left()`, otherwise it corresponds to | ||
| :code:`op @ arr @ op.inv`. | ||
| Rotation | ||
| ======== | ||
| As already mentioned a common transformation are rotations. The representation of the | ||
| rotation operator depends on which basis we use. For example, for the T-matrix using the | ||
| spherical wave basis, such a rotation is represented by the Wigner D-matrix elements, | ||
| but for plane waves this would look different. In *treams* we can create such an | ||
| abstract rotation operator by using :class:`~treams.Rotate` | ||
| .. doctest:: | ||
| >>> r = Rotate(np.pi) | ||
| which can then be converted to a representation by calling it with the basis argument. | ||
| .. doctest:: | ||
| >>> r(basis=SphericalWaveBasis.default(1)) | ||
| PhysicsArray( | ||
| [[...]], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| ) | ||
| If it is multiplied with an array that defines the attribute `basis` it will | ||
| automatically take that attribute. | ||
| .. doctest:: | ||
| >>> t = PhysicsArray(np.eye(6), basis=SphericalWaveBasis.default(1)) | ||
| >>> r @ t @ r.inv | ||
| PhysicsArray( | ||
| [[...]], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| ) | ||
| Here, we also use the property `inv` to get the inverse rotation. Moreover, we for | ||
| instances of :class:`~treams.PhysicsArray` we can get the same result by calling the | ||
| correspondingly named attribute | ||
| >>> phi = 1 | ||
| >>> r = Rotate(phi) | ||
| >>> (r @ t @ r.inv == t.rotate(phi)).all() | ||
| True | ||
| which also has the methods `apply_left` and `apply_right` to only apply the operator | ||
| from one side. For some basis sets only rotations about the z-axis are possible, while | ||
| other basis sets allow rotations including all three Euler angles. | ||
| Translation | ||
| =========== | ||
| The next transformation that is implemented are translations where the parameter is the | ||
| Cartesian translation vector. | ||
| .. doctest:: | ||
| >>> t = PhysicsArray(np.eye(6), basis=SphericalWaveBasis.default(1), k0=1) | ||
| >>> t.translate([1, 2, 3]) | ||
| PhysicsArray( | ||
| [[...]], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| poltype='helicity', | ||
| ) | ||
| For the translation we have to specify the basis and the vacuum wave number. In the | ||
| result we can see that the default material of the embedding is vacuum and the default | ||
| polarization type is taken from :attr:`treams.config.POLTYPE`. | ||
| .. note:: | ||
| The rotation and translation operators applied to a spherical or cylindrical basis | ||
| with multiple positions, will rotate or translate each position independently from | ||
| the others. This results in block-diagonal matrices with respect to the different | ||
| positions in such a case. | ||
| Expand in a different basis | ||
| =========================== | ||
| The expansion in a different basis set is a little bit more complicated due to the | ||
| number of possible combinations of which basis set can be expanded in which other basis | ||
| sets. Therefore, we will treat each source basis set separately in the following. | ||
| Also, here the notion of abstract operator and concrete representation breaks down to | ||
| some extent because it makes little sense to first define an abstract expansion in, | ||
| e.g., spherical waves without specifying the relevant multipoles. Thus, the concrete | ||
| representation of the target basis is the argument of the operator. | ||
| Plane waves | ||
| ----------- | ||
| Plane waves can be expanded into a different set of plane waves and into regular | ||
| spherical and cylindrical waves. The expansion into a different set of plane waves | ||
| is basically just a matching of the wave vectors and polarizations. | ||
| .. doctest:: | ||
| >>> plw = plane_wave([0, 3, 4], [.5, .5], k0=5, material=1) | ||
| >>> Expand(PlaneWaveBasisByComp.default([[0, 3]])) @ plw | ||
| PhysicsArray( | ||
| [1., 1.], | ||
| basis=PlaneWaveBasisByComp( | ||
| kx=[0. 0.], | ||
| ky=[3. 3.], | ||
| pol=[1 0], | ||
| ), | ||
| k0=5.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='up', | ||
| ) | ||
| For example, here we change from the expansion in | ||
| :class:`~treams.PlaneWaveBasisByUnitVector` to the expansion by x- and y- components. | ||
| For such a basis change, it is necessary that the material and the wave number is | ||
| specified. | ||
| Next, we can expand this plane wave also in cylindrical and in spherical waves. | ||
| .. doctest:: | ||
| >>> Expand(CylindricalWaveBasis.default([4], 1)) @ plw | ||
| PhysicsArray( | ||
| [0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], | ||
| basis=CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| kz=[4. 4. 4. 4. 4. 4.], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=5.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| ) | ||
| >>> Expand(SphericalWaveBasis.default(1)) @ plw | ||
| PhysicsArray( | ||
| [ 0.307-0.j , -2.763+0.j , -0. -1.302j, -0. -1.302j, | ||
| -2.763+0.j , 0.307+0.j ], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=5.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| poltype='helicity', | ||
| ) | ||
| Spherical waves | ||
| --------------- | ||
| Next, we have spherical waves. In comparison to the plane waves, spherical waves have | ||
| the added difficulty of the categorization of "regular" and "singular" functions and the | ||
| distinction of global and local basis sets. | ||
| In a simple case we want to expand a spherical wave that is centered not at the origin | ||
| and expand it around the origin | ||
| .. doctest:: | ||
| >>> off_centered_swb = SphericalWaveBasis.default(1, positions=[[1, 0, 0]]) | ||
| >>> spw = spherical_wave(1, 0, 0, basis=off_centered_swb, k0=1, material=1, modetype="singular") | ||
| >>> ex = Expand(SphericalWaveBasis.default(1)) | ||
| >>> ex @ spw | ||
| PhysicsArray( | ||
| [ 0. +0.j , 0. +0.319j, 0. +0.j , 0.81+0.j , | ||
| 0. +0.j , -0. +0.319j], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='singular', | ||
| poltype='helicity', | ||
| ) | ||
| We defined the wave as a singular wave and, if nothing is explicitly specified, the | ||
| expansion into other spherical waves is taken as the same type of field. So, a singular | ||
| field will be expanded again in singular modes and a regular field is expanded in | ||
| regular modes. However, we can also change the type of mode, when the field is expanded | ||
| around a different origin | ||
| .. doctest:: | ||
| >>> ex = Expand(SphericalWaveBasis.default(1), "regular") | ||
| >>> ex @ spw | ||
| PhysicsArray( | ||
| [0. +0.j , 1.466+0.319j, 0. +0.j , 0.81 +1.262j, | ||
| 0. +0.j , 1.466+0.319j], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| poltype='helicity', | ||
| ) | ||
| for this we had to define the ``modetype`` for the expand operator. | ||
| Next, we want to look at the expansion of a global field into a local field at multiple | ||
| origins, which works quite similarly | ||
| .. doctest:: | ||
| >>> sw_global = spherical_wave(1, 0, 0, k0=1, material=1, modetype="regular") | ||
| >>> local_swb = SphericalWaveBasis.default(1, 2, positions=[[0, 0, 1], [0, 0, -1]]) | ||
| >>> sw_global.expand.apply_left(local_swb) | ||
| PhysicsArray( | ||
| [0. +0.j, 0. +0.j, 0. +0.j, 0.904-0.j, 0. +0.j, 0. +0.j, | ||
| 0. +0.j, 0. +0.j, 0. +0.j, 0.904-0.j, 0. +0.j, 0. +0.j], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 1 1 1 1 1 1], | ||
| l=[1 1 1 1 1 1 1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[ 0. 0. 1.], [ 0. 0. -1.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| poltype='helicity', | ||
| ) | ||
| For the translations within only regular or only singular waves it is possible to | ||
| expand back into the same basis set in this case corresponds to the multiplication by a | ||
| unit matrix. | ||
| .. doctest:: | ||
| >>> sw_global.expand(SphericalWaveBasis.default(1)) | ||
| PhysicsArray( | ||
| [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| poltype='helicity', | ||
| ) | ||
| For translations from singular to regular waves, the same basis set means that a | ||
| zero matrix is returned. | ||
| .. doctest:: | ||
| >>> sw_global.expand.apply_right(SphericalWaveBasis.default(1), "singular") | ||
| PhysicsArray( | ||
| [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='singular', | ||
| poltype='helicity', | ||
| ) | ||
| Besides that the expansion of spherical waves in different basis sets results in dense | ||
| matrices. | ||
| The expansion of spherical waves into cylindrical or plane waves is a continuous | ||
| spectrum and is currently not implemented. | ||
| Cylindrical waves | ||
| ----------------- | ||
| Cylindrical waves are similar to spherical waves, in the sense, that they can be | ||
| separated into regular and singular modes and that they can be defined with multiple | ||
| origins within treams. Therefore, the expansion within cylindrical waves follows the | ||
| same properties than spherical waves. | ||
| .. doctest:: | ||
| >>> off_centered_cwb = CylindricalWaveBasis.default([0], 1, positions=[[1, 0, 0]]) | ||
| >>> cyw = cylindrical_wave(0, 1, 0, basis=off_centered_cwb, k0=1, material=1, modetype="singular") | ||
| >>> ex = Expand(CylindricalWaveBasis.default([0], 1)) | ||
| >>> ex @ cyw | ||
| PhysicsArray( | ||
| [ 0. +0.j, 0.115-0.j, 0. +0.j, -0.44 +0.j, 0. +0.j, | ||
| 0.765+0.j], | ||
| basis=CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| kz=[0. 0. 0. 0. 0. 0.], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='singular', | ||
| ) | ||
| Additionally, it is possible to expand a cylindrical wave into spherical waves. Note, | ||
| that waves defined with multiple origins get each expanded separately. The positions | ||
| of the spherical and cylindrical waves must be equal. | ||
| .. doctest:: | ||
| >>> cyw = cylindrical_wave(0, 1, 0, k0=1, material=1, modetype="regular") | ||
| >>> cyw.expand(SphericalWaveBasis.default(1)) | ||
| PhysicsArray( | ||
| [0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j, 3.07+0.j], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| poltype='helicity', | ||
| ) | ||
| The inverse of this expansion is not implemented. | ||
| The expansion of cylindrical waves into plane waves is a continuous spectrum and is not | ||
| implemented. | ||
| Expand in a different basis with periodic boundaries | ||
| ==================================================== | ||
| There is a special case of expansion implemented for the case of periodic boundaries | ||
| when using spherical or cylindrical waves. These expansions are needed to compute the | ||
| electromagnetic interaction between particles within a lattice. It is assumed that the | ||
| given basis with singular modes are repeated periodically in the given lattice | ||
| structure. Then, these fields are expanded as regular fields in a single unit cell. | ||
| .. doctest:: | ||
| >>> cyw = cylindrical_wave(0, 1, 0, k0=1, material=1, modetype="singular") | ||
| >>> cyw.expandlattice(1, 0) | ||
| PhysicsArray( | ||
| [0.+0.j , 2.-3.866j, 0.+0.j , 0.+0.j , 0.+0.j , 1.+1.234j], | ||
| basis=CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| kz=[0. 0. 0. 0. 0. 0.], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| kpar=WaveVector(0, nan, 0.0), | ||
| lattice=Lattice(1.0, alignment='x'), | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| ) | ||
| >>> spw = spherical_wave(1, 0, 0, k0=1, material=1) | ||
| >>> spw.expandlattice([1, 2], [0, 0]) | ||
| PhysicsArray( | ||
| [ 0.+0.j , 0.+0.j , 0.+0.j , -1.+7.722j, 0.+0.j , | ||
| 0.+0.j ], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| kpar=WaveVector(0, 0, nan), | ||
| lattice=Lattice([[1. 0.] | ||
| [0. 2.]], alignment='xy'), | ||
| material=Material(1, 1, 0), | ||
| modetype='regular', | ||
| poltype='helicity', | ||
| ) | ||
| The inverse of this operator is not implemented. Additionally, it's possible to expand | ||
| the periodic field into a different basis set. Spherical waves in a one-dimensional | ||
| lattice along the z-axis can be expanded in cylindrical waves | ||
| .. doctest:: | ||
| >>> spw = spherical_wave(1, 0, 0, k0=1, material=1) | ||
| >>> ex = ExpandLattice(basis=CylindricalWaveBasis.diffr_orders([.1], 0, 7, 1)) | ||
| >>> ex @ spw | ||
| PhysicsArray( | ||
| [ 0.+0.j , -0.+0.094j, 0.+0.j , -0.+0.154j, 0.+0.j , | ||
| -0.+0.011j], | ||
| basis=CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| kz=[-0.798 -0.798 0.1 0.1 0.998 0.998], | ||
| m=[0 0 0 0 0 0], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| k0=1.0, | ||
| kpar=WaveVector(nan, nan, 0.1), | ||
| lattice=Lattice(7.0, alignment='z'), | ||
| material=Material(1, 1, 0), | ||
| modetype='singular', | ||
| poltype='helicity', | ||
| ) | ||
| where the lattice and the wave vector are implicitly defined by the use of the | ||
| class method :func:`treams.CylindricalWaveBasis.diffr_orders`. Similarly, spherical | ||
| waves in a two-dimensional lattice in the x-y-plane can be expanded in plane waves. | ||
| .. doctest:: | ||
| >>> ex = ExpandLattice(basis=PlaneWaveBasisByComp.diffr_orders([.1, 0], [7, 7], 1)) | ||
| >>> ex @ spw | ||
| PhysicsArray( | ||
| [ 0.+0.j , -0.+0.004j, 0.+0.j , -0.+0.093j, 0.+0.j , | ||
| -0.+0.093j, 0.+0.j , -0.+0.638j, 0.+0.j , -0.+0.059j], | ||
| basis=PlaneWaveBasisByComp( | ||
| kx=[ 0.1 0.1 0.1 0.1 0.1 0.1 0.998 0.998 -0.798 -0.798], | ||
| ky=[ 0. 0. 0.898 0.898 -0.898 -0.898 0. 0. 0. 0. ], | ||
| pol=[1 0 1 0 1 0 1 0 1 0], | ||
| ), | ||
| k0=1.0, | ||
| kpar=WaveVector(0.1, 0, nan), | ||
| lattice=Lattice([[7. 0.] | ||
| [0. 7.]], alignment='xy'), | ||
| material=Material(1, 1, 0), | ||
| modetype='up', | ||
| poltype='helicity', | ||
| ) | ||
| Cylindrical waves, that themselves are periodic in the z-direction, in a one-dimensional | ||
| lattice along the x-axis can also be expanded in plane waves. | ||
| .. doctest:: | ||
| >>> cyw = cylindrical_wave(0, 1, 0, k0=1, material=1) | ||
| >>> ex = ExpandLattice(basis=PlaneWaveBasisByComp.diffr_orders([0, .1], Lattice([7, 7], "zx"), 1)) | ||
| >>> ex @ cyw | ||
| PhysicsArray( | ||
| [0. +0.j , 0.286-0.029j, 0. +0.j , 0.286-4.115j, | ||
| 0. +0.j , 0.286+0.378j, 0. +0.j , 0. +0.j , | ||
| 0. +0.j , 0. +0.j ], | ||
| basis=PlaneWaveBasisByComp( | ||
| kz=[ 0. 0. 0. 0. 0. 0. 0.898 0.898 -0.898 -0.898], | ||
| kx=[ 0.1 0.1 0.998 0.998 -0.798 -0.798 0.1 0.1 0.1 0.1 ], | ||
| pol=[1 0 1 0 1 0 1 0 1 0], | ||
| ), | ||
| k0=1.0, | ||
| kpar=WaveVector(nan, nan, 0.0), | ||
| lattice=Lattice(7.0, alignment='x'), | ||
| material=Material(1, 1, 0), | ||
| modetype='up', | ||
| ) | ||
| Change the polarization type | ||
| ============================ | ||
| Changing the polarization type is a simple operation. All waves can be expanded in | ||
| modes of well-defined helicity. For an achiral material these waves can equally be | ||
| expressed in modes of well-defined parity. The change between those polarization types | ||
| can be expressed as an operator. | ||
| .. doctest:: | ||
| >>> spw = spherical_wave(1, 0, 0, poltype="helicity") | ||
| >>> spw.changepoltype("parity") | ||
| PhysicsArray( | ||
| [ 0. , 0. , 0.707, -0.707, 0. , 0. ], | ||
| basis=SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ), | ||
| poltype='parity', | ||
| ) | ||
| Permute the axes | ||
| ================ | ||
| The permute operator is only implemented for plane waves, in particular for plane | ||
| waves that are defined by two of their components (and a direction of the modes). | ||
| For this type of waves, the rotation is only implemented about the z-axis. These | ||
| rotations then don't include a relabeling of the Cartesian axes, for example | ||
| :math:`(x', y', z') = (z, x, y)`. This operation is implemented separately as | ||
| permutation, meaning the axes labels get permuted. | ||
| .. doctest:: | ||
| >>> plw = plane_wave([2, 3, 6], 0) | ||
| >>> plw | ||
| PhysicsArray( | ||
| [0, 1], | ||
| basis=PlaneWaveBasisByUnitVector( | ||
| qx=[0.286 0.286], | ||
| qy=[0.429 0.429], | ||
| qz=[0.857 0.857], | ||
| pol=[1 0], | ||
| ), | ||
| ) | ||
| >>> plw.permute() | ||
| PhysicsArray( | ||
| [ 0. +0.j , -0.789+0.614j], | ||
| basis=PlaneWaveBasisByUnitVector( | ||
| qx=[0.857 0.857], | ||
| qy=[0.286 0.286], | ||
| qz=[0.429 0.429], | ||
| pol=[1 0], | ||
| ), | ||
| poltype='helicity', | ||
| ) | ||
| Evaluate the field | ||
| ================== | ||
| From a programming perspective, the evaluation of the field values at specified points | ||
| is also implemented by a couple of operators. The electric field :math:`\boldsymbol E`, | ||
| the magnetic field :math:`\boldsymbol H`, the displacement field :math:`\boldsymbol D`, | ||
| and the magnetic flux density :math:`\boldsymbol B` can be computed as well as two | ||
| different definitions of the Riemann-Silberstein vectors | ||
| :math:`\sqrt{2} \boldsymbol G_\pm = \boldsymbol E \pm \mathrm i Z_0 Z \boldsymbol H` and | ||
| :math:`\sqrt{2} \boldsymbol F_\pm = \frac{1}{\epsilon_0 \epsilon} \boldsymbol D \pm | ||
| \mathrm i \frac{c}{n} \boldsymbol B = \frac{n \pm \kappa}{n} G_\pm` (see also | ||
| :doc:`maxwell`). | ||
| .. doctest:: | ||
| >>> spw = spherical_wave(1, 0, 0, k0=1, material=1, poltype="helicity", modetype="regular") | ||
| >>> spw.efield([[0, 0, 0], [1, 0, 0]]) | ||
| PhysicsArray( | ||
| [[0.+0.j , 0.+0.j , 0.+0.163j], | ||
| [0.-0.j , 0.-0.074j, 0.+0.132j]], | ||
| ) |
-393
| .. testsetup:: | ||
| import numpy as np | ||
| import treams | ||
| ==================================== | ||
| Basis sets and other core parameters | ||
| ==================================== | ||
| Throughout the high-level functions and classes of *treams* a set of parameters appear | ||
| that define important underlying quantities for the calculation. First, these are the | ||
| different basis sets that are used to solve scattering processes: the spherical, | ||
| cylindrical, and plane wave solutions. Closely related to these basis sets are the | ||
| polarization types and the mode types. The other parameters are the vacuum wave numbers | ||
| and the materials as well as, in the case of calculations with periodicity involved, | ||
| the lattice definitions and the phase shift between lattice sites. | ||
| Basis sets | ||
| ========== | ||
| As described in :doc:`maxwell` it is possible to solve Maxwells equations in different | ||
| coordinate systems. While being in principle equivalent, for different scenarios it is | ||
| beneficial to use suitable solution sets that represent the waves with sufficient | ||
| precision when truncated to a finite number of modes. The chosen finite number of | ||
| modes is given in the classes :class:`~treams.SphericalWaveBasis`, | ||
| :class:`~treams.CylindricalWaveBasis`, and :class:`~treams._core.PlaneWaveBasis`, which | ||
| are all children of the base call :class:`~treams._core.BasisSet`. | ||
| The modes of the spherical basis can are defined by their degree ``l``, the order ``m``, | ||
| and an index for the polarization ``pol``. The basis is then simply the collection of | ||
| multiple of these modes, each given in a tuple with exactly that order, for example | ||
| .. doctest:: | ||
| >>> treams.SphericalWaveBasis([(1, -1, 0), (1, 0, 0), (1, 1, 0)]) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0], | ||
| l=[1 1 1], | ||
| m=[-1 0 1], | ||
| pol=[0 0 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| results in a basis with three modes. All have the same degree and polarization, but the | ||
| order ``m`` goes from -1 to 1. We see that there are also the fields ``pidx`` and | ||
| ``positions``. This is a special case for the spherical (and later also the | ||
| cyclindrical) wave basis. Sometimes, the fields are not expanded with respect to a | ||
| single point, but multiple positions. Then ``positions`` contains the their Cartesian | ||
| coordinates and ``pidx`` maps each mode to one of those coordinates. Here, the default | ||
| value of the expansion about a single origin is used. These basis sets behave mostly | ||
| like regular Python sets, we can check for example if a mode is in our basis set by | ||
| .. doctest:: | ||
| >>> (0, 1, 0, 0) in treams.SphericalWaveBasis([(1, -1, 0), (1, 0, 0), (1, 1, 0)]) | ||
| True | ||
| Equally, it is possible to use the regular comparisons and binary operators of Python | ||
| sets | ||
| .. doctest:: | ||
| >>> treams.SphericalWaveBasis([(1, -1, 0), (1, 0, 0), (1, 1, 0)]) > {(0, 1, 0, 0)} | ||
| True | ||
| >>> treams.SphericalWaveBasis([(1, -1, 0), (1, 0, 0), (1, 1, 0)]) & {(0, 1, 0, 0)} | ||
| SphericalWaveBasis( | ||
| pidx=[0], | ||
| l=[1], | ||
| m=[0], | ||
| pol=[0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| However, because we want to use those basis sets later to index the rows and columns of | ||
| matrices, the order of the entries is fixed. Therefore, the equality operator is | ||
| stricter. Two basis sets are only considered equal when they have the same number modes | ||
| in the same order and the same positions. | ||
| .. doctest:: | ||
| >>> treams.SphericalWaveBasis([(1, 0, 0), (1, 1, 0)]) == treams.SphericalWaveBasis([(1, 1, 0), (1, 0, 0)]) | ||
| False | ||
| For convenience it is possible to create a default order up to a maximal multipolar | ||
| order | ||
| .. doctest:: | ||
| >>> treams.SphericalWaveBasis.default(2) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2], | ||
| m=[-1 -1 0 0 1 1 -2 -2 -1 -1 0 0 1 1 2 2], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| where we now have a spherical wave basis up do quadrupolar order. | ||
| The cyclindrical wave basis is mostly similar to the quadrupolar basis. Instead of a the | ||
| multipole ``l`` the z-component of the wave vector ``kz`` is used | ||
| .. doctest:: | ||
| >>> treams.CylindricalWaveBasis([(.1, -1, 0), (.1, 0, 0), (.1, 1, 0)]) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0], | ||
| kz=[0.1 0.1 0.1], | ||
| m=[-1 0 1], | ||
| pol=[0 0 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| which is a real number. The default function takes a list of ``kz`` values a maximal | ||
| absolute value for ``m``. | ||
| .. doctest:: | ||
| >>> treams.CylindricalWaveBasis.default([-.5, .5], 1) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0], | ||
| kz=[-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 0.5 0.5 0.5 0.5 0.5 0.5], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| The cylindrical wave basis is particularly useful for systems with periodicity in the | ||
| z-direction. Then, a basis with the diffraction orders up to a threshold can be obtained | ||
| by running | ||
| .. doctest:: | ||
| >>> treams.CylindricalWaveBasis.diffr_orders(kz=.1, mmax=1, lattice=2 * np.pi, bmax=1.05) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| kz=[-0.9 -0.9 -0.9 -0.9 -0.9 -0.9 0.1 0.1 0.1 0.1 0.1 0.1 1.1 1.1 | ||
| 1.1 1.1 1.1 1.1], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| where ``bmax`` defines a distance in reciprocal space. | ||
| The plane wave basis behaves a little bit different. First, it is currently only defined | ||
| with respect to a single origin so the ``pidx`` and ``positions`` is not defined. Also, | ||
| the basis can be defined in two ways: :class:`PlaneWaveBasisByUnitVector` and | ||
| :class:`PlaneWaveBasisByComp`. In the first case, the definition is given by the unit | ||
| vector which, multiplied by the wave number in the medium, gives the full wave vector. | ||
| In the second case, two components of the wave vector are given and the remaining third | ||
| Cartesian component is defined such that it fulfils the dispersion relation. | ||
| .. doctest:: | ||
| >>> treams.PlaneWaveBasisByUnitVector([(4, 0, 3, 0), (4, 0, 3, 1)]) | ||
| PlaneWaveBasisByUnitVector( | ||
| qx=[0.8 0.8], | ||
| qy=[0. 0.], | ||
| qz=[0.6 0.6], | ||
| pol=[0 1], | ||
| ) | ||
| >>> treams.PlaneWaveBasisByComp([(1, 0, 0), (1, 0, 1)]) | ||
| PlaneWaveBasisByComp( | ||
| kx=[1 1], | ||
| ky=[0 0], | ||
| pol=[0 1], | ||
| ) | ||
| By default, it is assumed, that the x- and y- components are given for the latter class, | ||
| but other components can also be chosen. | ||
| It is possible to convert between those basis sets by using the corresponding | ||
| functions | ||
| .. doctest:: | ||
| >>> pwbc = treams.PlaneWaveBasisByComp([(3, 0, 0), (3, 0, 1)]) | ||
| >>> pwbc.byunitvector(5) | ||
| PlaneWaveBasisByUnitVector( | ||
| qx=[0.6+0.j 0.6+0.j], | ||
| qy=[0.+0.j 0.+0.j], | ||
| qz=[0.8+0.j 0.8+0.j], | ||
| pol=[0 1], | ||
| ) | ||
| >>> pwbuv = treams.PlaneWaveBasisByUnitVector([(0, 0, 1, 0), (0, 0, 1, 1)]) | ||
| >>> pwbuv.bycomp(1) | ||
| PlaneWaveBasisByComp( | ||
| kx=[0. 0.], | ||
| ky=[0. 0.], | ||
| pol=[0 1], | ||
| ) | ||
| Additionally, similar to the case of cylindrical waves, the basis by components can be | ||
| used for a range of diffraction orders | ||
| >>> treams.PlaneWaveBasisByComp.diffr_orders([0, 0], np.eye(2), 7) | ||
| PlaneWaveBasisByComp( | ||
| kx=[ 0. 0. 0. 0. 0. 0. 6.283 6.283 -6.283 -6.283], | ||
| ky=[ 0. 0. 6.283 6.283 -6.283 -6.283 0. 0. 0. 0. ], | ||
| pol=[1 0 1 0 1 0 1 0 1 0], | ||
| ) | ||
| Polarizations | ||
| ============= | ||
| The definitions of the basis sets above are not complete without specifying the | ||
| polarization types. In *treams* two polarization types are supported: `helicity` and | ||
| `parity`. The first allows the use of chiral material parameters. Each polarization type | ||
| contains two polarizations that are indicated by the integers `0` and `1` throughout the | ||
| code. For helicity polarizations `0` stands for negative helicity and `1` for positive | ||
| helicity. In the case of parity polarizations `0` stands for `TE` or `magnetic` | ||
| polarization and `1` for `TM` or `electric` polarizations. The magnetic parity waves are | ||
| defined in :func:`treams.special.vsw_M`, :func:`treams.special.vsw_rM`, | ||
| :func:`treams.special.vcw_M`, :func:`treams.special.vcw_rM`, and | ||
| :func:`treams.special.vpw_M`. For spherical waves they are transverse with respect to | ||
| the radial direction, for cylindrical and plane waves they are transverse to the z-axis. | ||
| The corresponding electric parity waves are :func:`treams.special.vsw_N`, | ||
| :func:`treams.special.vsw_rN`, :func:`treams.special.vcw_N`, | ||
| :func:`treams.special.vcw_rN`, and :func:`treams.special.vpw_N`. | ||
| The helicity waves are defined in :func:`treams.special.vsw_A`, | ||
| :func:`treams.special.vsw_rA`, :func:`treams.special.vcw_A`, | ||
| :func:`treams.special.vcw_rA`, and :func:`treams.special.vpw_A`. | ||
| The default polarization type to be used can by setting ``treams.config.POLTYPE`` to the | ||
| corresponding string. | ||
| Mode types | ||
| ========== | ||
| For some basis sets there exist two different types of modes, that distinguish | ||
| propagation features. For the spherical and cylindrical basis theses are `regular` | ||
| and `singular` modes. The former come through the use of (spherical) Bessel Functions | ||
| and the latter through the use of (spherical) Hankel functions of the first kind. The | ||
| regular modes are finite in the whole space. Thus, they are suitable for describing | ||
| incident modes or to expand a plane wave. The singular modes fulfil the radiation | ||
| condition and as such are used for the scattered fields. | ||
| For the plane wave basis of type (:class:`~treams.PlaneWaveBasisByComp`) only two | ||
| components of the wave vector are given and the third component is only implicitly | ||
| defined by the wave number and the material parameters. The application for this basis | ||
| is mostly within stratified media that are uniform or periodic in the two other | ||
| dimensions. Thus, the two given components of the wave vectors are conserved up to | ||
| reciprocal lattice vectors. To lift the ambiguity of the definition of the third | ||
| component, the mode types `up` and `down` are possible. They define, if the modes | ||
| propagate -- or decay for evanescent modes -- along the positive or negative direction | ||
| with respect to the third axis. | ||
| Vacuum wave number | ||
| ================== | ||
| All calculations are executed in frequency domain. Instead of defining the frequency | ||
| :math:`\nu` or the angular frequency :math:`\omega` itself, *treams* works by using the | ||
| vacuum wave number | ||
| .. math:: | ||
| k_0 = \frac{2 \pi \nu}{c} = \frac{\omega}{c} | ||
| where :math:`c` is the speed of light in vacuum. In the code this real-valued number is | ||
| usually referred to by ``k0``. Implicitly, it is assumed throughout that all quantities, | ||
| like wave numbers, wave vectors, distances, or lattice vectors are given in the same | ||
| unit of (inverse) length. | ||
| Materials | ||
| ========= | ||
| For materials there exists the class :class:`~treams.Material`, which holds the values | ||
| of the relative permittivity, relative permeability, and the chirality parameter. The | ||
| default material is air and can be initialized without any parameters. For other cases, | ||
| the parameters can be given in the order above. | ||
| .. doctest:: | ||
| >>> treams.Material() | ||
| Material(1, 1, 0) | ||
| >>> treams.Material(3, 2, 1) | ||
| Material(3, 2, 1) | ||
| It's also possible to get the parameters from the refractive index (or the refractive | ||
| indices for negative and positive helicity) and the impedance | ||
| .. doctest:: | ||
| >>> treams.Material.from_n(3) | ||
| Material(9.0, 1.0, 0) | ||
| >>> treams.Material.from_nmp((3, 5)) | ||
| Material(16.0, 1.0, 1.0) | ||
| Lattices | ||
| ======== | ||
| The periodicity of arrangements is given by defining an instance of the class | ||
| :class:`~treams.Lattice`. A lattice can be one-, two-, or three-dimensional. | ||
| .. doctest:: | ||
| >>> treams.Lattice(1) | ||
| Lattice(1.0, alignment='z') | ||
| >>> treams.Lattice([[1, .5], [-.5, 1]]) | ||
| Lattice([[ 1. 0.5] | ||
| [-0.5 1. ]], alignment='xy') | ||
| >>> treams.Lattice([1, 2, 3]) | ||
| Lattice([[1. 0. 0.] | ||
| [0. 2. 0.] | ||
| [0. 0. 3.]], alignment='xyz') | ||
| The one- and two-dimensional lattices have to be aligned with one and two, respectively, | ||
| Cartesian axes. The default alignments are along the z-axis for one-dimensional and in | ||
| the x-y-plane for the two-dimensional lattices. In the last example we see that it is | ||
| sufficient to just specify the diagonal entries. It's also possible to automatically | ||
| create special lattice shapes, for example | ||
| .. doctest:: | ||
| >>> treams.Lattice.hexagonal(2) | ||
| Lattice([[2. 0. ] | ||
| [1. 1.732]], alignment='xy') | ||
| creates a hexagonal lattice with sidelength 2. It's also possible to extract a | ||
| lower-dimensional sublattice | ||
| .. doctest:: | ||
| >>> lat_3d = treams.Lattice([1, 2, 3]) | ||
| >>> treams.Lattice(lat_3d, "zx") | ||
| Lattice([[0. 1.] | ||
| [3. 0.]], alignment='zx') | ||
| or to combine and compare lattices | ||
| .. doctest:: | ||
| >>> treams.Lattice(1, "x") | treams.Lattice(2, "y") | ||
| Lattice([[1. 0.] | ||
| [0. 2.]], alignment='xy') | ||
| >>> treams.Lattice([1, 2], "xy") & treams.Lattice([2, 3], "yz") | ||
| Lattice(2.0, alignment='y') | ||
| >>> treams.Lattice(1, "x") <= treams.Lattice([1, 2], "xy") | ||
| True | ||
| The volume of the lattice can also be obtained | ||
| .. doctest:: | ||
| >>> treams.Lattice([[1, 0], [0, 1]]).volume | ||
| 1.0 | ||
| >>> treams.Lattice([[0, 1], [1, 0]]).volume | ||
| -1.0 | ||
| as we see the volume is "signed", i.e. it shows if the lattice vectors are in a | ||
| right-handed order, and the reciprocal lattice vectors can be computed | ||
| .. doctest:: | ||
| >>> treams.Lattice([1, 1]).reciprocal | ||
| array([[ 6.283, -0. ], | ||
| [-0. , 6.283]]) | ||
| Phase vector | ||
| ============ | ||
| The wave vector, often referred to as ``kpar``, specifies the phase relationship of | ||
| different lattice sites :math:`\exp(\mathrm i \boldsymbol k_\parallel \boldsymbol R)`. | ||
| .. doctest:: | ||
| >>> treams.WaveVector() | ||
| WaveVector(nan, nan, nan) | ||
| >>> treams.WaveVector(1) | ||
| WaveVector(nan, nan, 1) | ||
| >>> treams.WaveVector(1, "x") | ||
| WaveVector(1, nan, nan) | ||
| >>> treams.WaveVector((1, 2)) | ||
| WaveVector(1, 2, nan) | ||
| >>> treams.WaveVector((1, 2, 3)) | ||
| WaveVector(1, 2, 3) | ||
| where unspecified directions are represented as ``nan``. The wave vectors can be | ||
| combined and compared. | ||
| .. doctest:: | ||
| >>> treams.WaveVector((1, 2)) | treams.WaveVector((2, 3), "yz") | ||
| WaveVector(nan, 2, nan) | ||
| >>> treams.WaveVector(1, "x") & treams.WaveVector(2, "y") | ||
| WaveVector(1, 2, nan) | ||
| >>> treams.WaveVector(1, "x") >= treams.WaveVector((1, 2)) | ||
| True | ||
| Note that the ordering is from less strict wave vector to the stricter one. |
| ==================== | ||
| Physics-aware arrays | ||
| ==================== | ||
| .. testsetup:: | ||
| import numpy as np | ||
| import treams | ||
| A core building block of the underlying features of treams are physics-aware arrays. | ||
| In most of their properties they behave similar to numpy arrays and one can easily | ||
| change the type and mix them | ||
| .. doctest:: | ||
| >>> np.array([1, 2]) * treams.PhysicsArray([2, 3]) | ||
| PhysicsArray( | ||
| [2, 6], | ||
| ) | ||
| >>> np.array([1, 2]) @ treams.PhysicsArray([2, 3]) | ||
| 8 | ||
| but they have mainly two features added. First, they derive from | ||
| :class:`treams.util.AnnotatedArray` so they can carry annotations with them, but these | ||
| annotations are restricted to the physical quantities (as described in :doc:`params`). | ||
| Second, they offer special methods to create matrices for common transformations like | ||
| rotations, which are described in more detail in :doc:`operators`. | ||
| Special properties | ||
| ================== | ||
| .. doctest:: | ||
| >>> treams.PhysicsArray([[0, 1], [2, 3]], k0=(1, 2)) | ||
| PhysicsArray( | ||
| [[0, 1], | ||
| [2, 3]], | ||
| k0=(1.0, 2.0), | ||
| ) | ||
| In this example you can notice that the values for the vacuum wave number ``k0`` were | ||
| converted from integers to floats. Thus, trying to use | ||
| :code:`tream.PhysicsArray([1], k0=1j)` will raise an error, because the complex number | ||
| cannot be interpreted as a float. Additional special keywords are `basis`, `kpar`, | ||
| `lattice`, `material`, `modetype`, and `poltype`. These properties can also be accessed | ||
| by setting the corresponding attribute | ||
| .. doctest:: | ||
| >>> m = treams.PhysicsArray([1, 2]) | ||
| >>> m.material = 4 | ||
| >>> m | ||
| PhysicsArray( | ||
| [1, 2], | ||
| material=Material(4, 1, 0), | ||
| ) | ||
| where we now have a material with the relative permittivity 4. As with its parent class | ||
| these properties are also compared and merged when using operations on these objects | ||
| .. doctest:: | ||
| >>> treams.PhysicsArray([0, 1], k0=1) + treams.PhysicsArray([2, 3], material=2) | ||
| PhysicsArray( | ||
| [2, 4], | ||
| k0=1.0, | ||
| material=Material(2, 1, 0), | ||
| ) | ||
| and using conflicting values will raise a warning, for example | ||
| :code:`treams.PhysicsArray([0, 1], k0=1) + treams.PhysicsArray([2, 3], k0=2)` | ||
| emits :code:`treams/util.py:249: AnnotationWarning: at index 0: overwriting key 'k0'`. | ||
| The special properties have also a unique behavior when appearing in matrix | ||
| multiplications. If one of the two matrices has the special property not set, it becomes | ||
| "transparent" to it. Check out the difference between | ||
| .. doctest:: | ||
| >>> np.ones((2, 2)) @ treams.PhysicsArray([1, 2], k0=1.0) | ||
| PhysicsArray( | ||
| [3., 3.], | ||
| k0=1.0, | ||
| ) | ||
| and | ||
| .. doctest:: | ||
| >>> np.ones((2, 2)) @ treams.util.AnnotatedArray([1, 2], k0=(1.0,)) | ||
| AnnotatedArray( | ||
| [3., 3.], | ||
| AnnotationSequence(AnnotationDict({})), | ||
| ) | ||
| where besides the obvious difference in array types, the property `k0` is preserved. | ||
| The full list of special properties is: | ||
| ======== ======================================================= | ||
| Name Description | ||
| ======== ======================================================= | ||
| basis Basis set: spherical, cylindrical, planar | ||
| k0 Vacuum wave number | ||
| kpar Phase relation in lattices (:class:`treams.WaveVector`) | ||
| lattice Definition of a lattice (:class:`treams.Lattice`) | ||
| modetype Modetype, depends on wave (:ref:`params:Mode types`) | ||
| material Embedding material (:class:`treams.Material`) | ||
| poltype Polarization types (:ref:`params:Polarizations`) | ||
| ======== ======================================================= |
-124
| .. highlight:: python | ||
| .. only:: builder_html | ||
| ========================== | ||
| S-Matrices for plane waves | ||
| ========================== | ||
| In addition to the T-matrix method, where incident and scattered fields are related, | ||
| S-matrices relate incoming and outgoing fields. To describe scattering in the plane | ||
| wave basis, we use exactly such a S-matrix description. The incoming and outgoing waves | ||
| are defined with respect to a plane, typically the x-y-plane. This plane additionally | ||
| separates the whole space into two parts. This then separates the description into | ||
| four parts: the transmission of fields propagating in the positive and negative | ||
| z-direction, and the reflection of those fields. | ||
| Slabs | ||
| ===== | ||
| The main object for plane wave computations is the class :class:`~treams.SMatrices` | ||
| which exactly collects those four individual S-matrices. For simple interfaces and the | ||
| propagation in a homogeneous medium these S-matrices can be obtained analytically. | ||
| Combining these two objects then allows the description of simple slabs. | ||
| .. literalinclude:: examples/slab.py | ||
| :language: python | ||
| :lines: 6-14 | ||
| The setup is fairly simple. The materials are given in order from negative to positive | ||
| z-values. We simply loop over the wave number and calculate transmission and reflection | ||
| in the chiral medium for both helicites. | ||
| .. plot:: examples/slab.py | ||
| From T-matrix arrays | ||
| ==================== | ||
| While this example is simple we can build more complicated structures from | ||
| two-dimensional arrays of T-matrices. We take spheres on an thin film as an example. | ||
| This means we first calculate the S-matrices for the thin film and the array | ||
| individually and then couple those two systems. | ||
| .. literalinclude:: examples/array_spheres.py | ||
| :language: python | ||
| :lines: 6-12 | ||
| Beforehand, we define all the necessary parameters. First the wave numbers, then the | ||
| parameters of the slab, and finally those for the lattice and the spheres. Then, we can | ||
| use a simple loop to solve the system for all wave numbers. | ||
| .. literalinclude:: examples/array_spheres.py | ||
| :language: python | ||
| :lines: 14-27 | ||
| We set some oblique incidence and the array of spheres. Then, we define a linearly | ||
| polarized plane wave and the needed S-matrices: a slab, the distance between the | ||
| top interface of the slab to the center of the sphere array, and the array in the | ||
| S-matrix representation itself. | ||
| .. plot:: examples/array_spheres.py | ||
| From cylindrical T-matrix gratings | ||
| ================================== | ||
| Now, we want to perform a small test of the methods. Instead of creating the | ||
| two-dimensional sphere array right away, we intermediately create a one-dimensional | ||
| array, then calculate cylindrical T-matrices, and place them in a second | ||
| one-dimensional lattice, thereby, obtaining the S-matrix from the previous section. | ||
| .. literalinclude:: examples/array_spheres_tmatrixc.py | ||
| :language: python | ||
| :lines: 6-13 | ||
| The definition of the parameters is quite similar. We store the lattice pitch for later | ||
| use separately and define the maximal order :code:`mmax` separately. | ||
| .. literalinclude:: examples/array_spheres_tmatrixc.py | ||
| :language: python | ||
| :lines: 15-32 | ||
| The first half of the loop is now a little bit different. After creating the spheres | ||
| we solve the interaction along the z-direction, then create the cylindrical T-matrix | ||
| and finally calculate the interaction along the x-direction. The second half is the | ||
| same as in the previous calculation. | ||
| The most important aspect to note here, is that the method | ||
| :meth:`treams.SMatrix.from_array` implicitly converts the lattice in the z-x-plane to | ||
| a lattice in the x-y-plane. | ||
| .. plot:: examples/array_spheres_tmatrixc.py | ||
| Band structure | ||
| ============== | ||
| Finally, we want to compute the band structure of a system consisting of the periodic | ||
| repetition of an S-matrix along the z-direction. In principle, one can obtain this | ||
| band structure also from the lattice interaction in the T-matrix, but calculating it | ||
| from the S-matrix has two benefits. First, more complex systems can be analyzed, because | ||
| slabs and objects described by cylindrical T-matrices can be included. Second, one only | ||
| defines :math:`k_0`, :math:`k_x`, and :math:`k_y`. Then, the result of the calculation | ||
| are all components :math:`k_z` and the plane wave decomposition of the polarization from | ||
| an eigenvalue decomposition. So, instead of a 4-dimensional parameter sweep only a | ||
| 3-dimensional sweep is necessary decreasing the computation time. The downside is, that | ||
| one is restricted to unit cells, where one vector points along the z-axis and is | ||
| perpendicular to the other two. | ||
| We take the array of spheres on top of a slab and continue this one infinitely along the | ||
| z-axis. Thus, the setup is | ||
| .. literalinclude:: examples/band_structure.py | ||
| :language: python | ||
| :lines: 6-13 | ||
| where :code:`az` is the length of the lattice vector pointing in the z-direction. With a | ||
| simple loop we can get the band structure for :math:`k_x = 0 = k_y`. | ||
| .. literalinclude:: examples/band_structure.py | ||
| :language: python | ||
| :lines: 15-31 | ||
| which looks as follows, after a cut on the imaginary part of the :math:`k_z` component. | ||
| .. plot:: examples/band_structure.py |
| ====== | ||
| Theory | ||
| ====== | ||
| These sections give an overview over theory that underlies the different aspects of | ||
| treams. For more in-depth information, there is also a list of associated | ||
| :ref:`about:Publications`. | ||
| .. toctree:: | ||
| :maxdepth: 1 | ||
| maxwell |
-208
| .. highlight:: python | ||
| .. only:: builder_html | ||
| ========== | ||
| T-Matrices | ||
| ========== | ||
| .. contents:: Table of contents | ||
| :local: | ||
| One of the main objects for electromagnetic scattering calculations within *treams* are | ||
| T-matrices. They describe the scattering response of an object by encoding the linear | ||
| relationship between incident and scattered fields. These fields are expanded using | ||
| the vector spherical wave functions. | ||
| The T-matrices of spheres or multiple layers of spherical shells, present for example in | ||
| core-shell particles, can be obtained analytically. For more complicated shapes | ||
| numerical methods are necessary to compute the T-matrix. Once the T-matrix of a single | ||
| object is known, the electromagnetic interaction between particle cluster can be | ||
| calculated efficiently. Such clusters can be analyzed in their local description, where | ||
| the field expansions are centered at each particle of the cluster, or in a global | ||
| description treating the whole cluster as a single object. | ||
| *treams* is particularly aimed at analyzing scattering within lattices. These lattices | ||
| can be periodic in one, two, or all three spatial dimensions. The unit cell of those | ||
| lattices can consist of an arbitrary number of objects described by a T-matrix. | ||
| Spheres | ||
| ======= | ||
| It's possible to calculate the T-matrix of a single (multi-layered) sphere with the | ||
| method :meth:`~treams.TMatrix.sphere`. We start by defining the relevant parameters for | ||
| our calculation and creating the T-matrices themselves. | ||
| .. literalinclude:: examples/sphere.py | ||
| :language: python | ||
| :lines: 6-10 | ||
| Now, we can easily access quantities like the scattering and extinction cross | ||
| sections | ||
| .. literalinclude:: examples/sphere.py | ||
| :language: python | ||
| :lines: 12-13 | ||
| From the parameter ``lmax = 4`` we see that the T-matrix is calculated up to the forth | ||
| multipolar order. To restrict the T-matrix to the dipolar coefficients only, we can | ||
| select a basis containing only those coefficients. | ||
| .. literalinclude:: examples/sphere.py | ||
| :language: python | ||
| :lines: 15-18 | ||
| Now, we can look at the results by plotting them and observe, unsurprisingly, that for | ||
| larger frequencies the dipolar approximation is not giving an accurate result. Finally, | ||
| we visualize the fields at the largest frequency. | ||
| .. literalinclude:: examples/sphere.py | ||
| :language: python | ||
| :lines: 20-29 | ||
| We select the T-matrix and illuminate it with a plane wave. Next, we set up the grid | ||
| and choose valid points, namely those that are outside of our spherical object. Then, | ||
| we can calculate the fields as a superposition of incident and scattered fields. | ||
| .. plot:: examples/sphere.py | ||
| Clusters | ||
| ======== | ||
| Multi-scattering calculations in a cluster of particles is a typical application of the | ||
| T-matrix method. We first construct an object from four different spheres placed at the | ||
| corners of a tetrahedron. Using the definition of the relevant parameters | ||
| .. literalinclude:: examples/cluster.py | ||
| :language: python | ||
| :lines: 6-17 | ||
| we can simply first create the spheres and put them together in a cluster, where we | ||
| immediately calculate the interaction. | ||
| .. literalinclude:: examples/cluster.py | ||
| :language: python | ||
| :lines: 19-20 | ||
| Then, we can illuminate with a plane wave and get the scattered field coefficients and | ||
| the scattering and extinction cross sections for that particular illumination. | ||
| .. literalinclude:: examples/cluster.py | ||
| :language: python | ||
| :lines: 22-24 | ||
| Finally, with few lines similar to the plotting of the field intensity of a single | ||
| sphere we can obtain the fields outside of the sphere | ||
| .. literalinclude:: examples/cluster.py | ||
| :language: python | ||
| :lines: 26-32 | ||
| Up to here, we did all calculations for the cluster in the local basis. By expanding | ||
| the incident and scattered fields in a basis with a single origin we can describe the | ||
| same object. Often, a larger number of multipoles is needed to do so and some | ||
| information on fields between the particles is lost. But, the description in a global | ||
| basis can be more efficient in terms of matrix size. | ||
| .. literalinclude:: examples/cluster.py | ||
| :language: python | ||
| :lines: 57-68 | ||
| A comparison of the calculated near-fields and the cross sections show good agreement | ||
| between the results of both, local and global, T-matrices. | ||
| .. plot:: examples/cluster.py | ||
| In the last figure, the T-matrix is rotated by 90 degrees about the y-axis and the | ||
| illumination is set accordingly to be a plane wave propagating in the negative | ||
| z-direction, such that the whole system is the same simply. It shows how the rotate | ||
| operator produces consistent results. | ||
| .. literalinclude:: examples/cluster.py | ||
| :language: python | ||
| :lines: 94-106 | ||
| One-dimensional arrays (along z) | ||
| ================================ | ||
| Next, we turn to systems that are periodic in the z-direction. We calculate the | ||
| scattering from an array of spheres. Intentionally, we choose a unit cell with two | ||
| spheres that overlap along the z-direction, but are not placed exactly along the same | ||
| line. This is the most general case for the implemented lattice sums. After the common | ||
| setup of the parameters we simply create a cluster in a local basis. | ||
| .. literalinclude:: examples/chain.py | ||
| :language: python | ||
| :lines: 6-12 | ||
| This time we let them interact specifying a one-dimensional lattice, so that the spheres | ||
| form a chain. | ||
| .. literalinclude:: examples/chain.py | ||
| :language: python | ||
| :lines: 14-15 | ||
| Next, we choose set the illumination to be propagating along the x-direction and to be | ||
| polarized along z. The z-component of the plane wave has to match to the wave vector | ||
| component of the lattice interaction. | ||
| .. literalinclude:: examples/chain.py | ||
| :language: python | ||
| :lines: 17-18 | ||
| There are efficient ways to calculate the many properties, especially in the far-field, | ||
| using cylindrical T-matrices. Those will be introduced in :doc:`tmatrixc`. Here, we will | ||
| stay in the expression of the fields as vector spherical waves. This allows the | ||
| calculation of the fields in the domain between the spheres. To get them accurately, we | ||
| expand the scattered fields in the whole lattice in dipolar approximation at each point | ||
| we want to probe. | ||
| .. literalinclude:: examples/chain.py | ||
| :language: python | ||
| :lines: 20-29 | ||
| Here, we plot the z-component of the electric field. Note, that the values at the top | ||
| and bottom side match exactly, as required due to the periodic boundary conditions. | ||
| .. plot:: examples/chain.py | ||
| Two-dimensional arrays (in the x-y-plane) | ||
| ========================================= | ||
| The case of periodicity in two directions is similar to the case of the previous section | ||
| with one-dimensional periodicity. Here, by convention the array has to be in the | ||
| x-y-plane. | ||
| .. literalinclude:: examples/grid.py | ||
| :language: python | ||
| :lines: 6-31 | ||
| With few changes we get the fields in a square array of the same spheres as in the | ||
| previous examples. Most importantly we changed the value of the variable :code:`lattice` | ||
| to an instance of a two-dimensional :class:`~treams.Lattice` and set :code:`kpar` | ||
| accordingly. Most other changes are just resulting from the change of the coordinate | ||
| system. | ||
| Here, we show the z-component of the electric field. | ||
| .. plot:: examples/grid.py | ||
| Three-dimensional arrays | ||
| ======================== | ||
| In a three-dimensional lattice we're mostly concerned with finding eigenmodes of a | ||
| crystal. We want to restrict the example to calculating modes at the gamma point in | ||
| reciprocal space. The calculated system consists of a single sphere in a cubic lattice. | ||
| In our very crude analysis, we blindly select the lowest singular value of the lattice | ||
| interaction matrix. Besides the mode when the frequency tends to zero, there are two | ||
| additional modes at higher frequencies in the chosen range. | ||
| .. literalinclude:: examples/crystal.py | ||
| :language: python | ||
| :lines: 6-17 | ||
| .. plot:: examples/crystal.py | ||
| .. highlight:: python | ||
| .. only:: builder_html | ||
| ====================== | ||
| Cylindrical T-matrices | ||
| ====================== | ||
| .. contents:: Table of contents | ||
| :local: | ||
| Here, we will cover cylindrical T-matrices, which are distinguished from the more | ||
| conventional (spherical) T-matrices through the use of vector cylindrical functions | ||
| instead of vector spherical functions. These waves are parametrized by the z-component | ||
| of the wave vector :math:`k_z`, that describes their behavior in the z-direction | ||
| :math:`\mathrm e^{\mathrm i k_z z}`, and the azimuthal order :math:`m`, that is also | ||
| used in vector spherical functions. Furthermore, there modes of well-defined parity and | ||
| helicity are available. | ||
| The cylindrical T-matrices are suited for structures that are periodic in one dimension | ||
| (conventionally set along the z-axis). Similarly to T-matrices of spheres that contain | ||
| the analytically known Mie coefficients, the cylindrical T-matrices for infinitely long | ||
| cylinders can also be calculated analytically. | ||
| Another similarity to spherical T-matrices are the possibilities to describe clusters of | ||
| objects in a local and global basis and to place these objects in a lattice. The | ||
| lattices can only extend in one and two dimensions; the z-direction is implicitly | ||
| periodic already. | ||
| (Infinitely long) Cylinders | ||
| =========================== | ||
| The first simple object, for which we'll calculate the cylindrical T-matrix is an | ||
| infinitely long cylinder. (Similar to the case of spheres and T-matrices those | ||
| cylinders could also have multiple concentric shells.) Due to the rotation symmetry | ||
| about the z-axis this matrix is diagonal with respect to :code:`m` and due to the | ||
| translation symmetry it is also diagonal with respect to :code:`kz`. | ||
| .. literalinclude:: examples/cylinder_tmatrixc.py | ||
| :language: python | ||
| :lines: 6-11 | ||
| For such infinitely long structures it makes more sense to talk about cross width | ||
| instead of cross section. We obtain the averaged scattering and extinction cross width | ||
| by | ||
| .. literalinclude:: examples/cylinder_tmatrixc.py | ||
| :language: python | ||
| :lines: 13-15 | ||
| Again, we can also select specific modes only, for example the modes with :math:`m = 0`. | ||
| .. literalinclude:: examples/cylinder_tmatrixc.py | ||
| :language: python | ||
| :lines: 16-19 | ||
| to calculate their cross width. Evaluating the field intensity in the x-y-plane is | ||
| equally simple. | ||
| .. literalinclude:: examples/cylinder_tmatrixc.py | ||
| :language: python | ||
| :lines: 16-19 | ||
| .. plot:: examples/cylinder_tmatrixc.py | ||
| Cylindrical T-matrices for one-dimensional arrays of spherical T-matrices | ||
| ========================================================================= | ||
| For our next example we want to look at the system of spheres on a one-dimensional | ||
| lattice again (:ref:`tmatrix:One-dimensional arrays (along z)`). They fulfil all | ||
| properties that define structures where the use of cylindrical waves is beneficial, | ||
| namely they have a finite extent in the x-y-plane and they are periodic along the | ||
| z-direction. | ||
| So, the initial setup of our calculation starts with spheres in the spherical wave | ||
| basis and place them in a chain. This is the same procedure as in | ||
| :ref:`tmatrix:One-dimensional arrays (along z)`. | ||
| .. literalinclude:: examples/chain_tmatrixc.py | ||
| :language: python | ||
| :lines: 6-15 | ||
| Next, we convert this chain in the spherical wave basis to a suitable cylindrical wave | ||
| basis. | ||
| .. literalinclude:: examples/chain_tmatrixc.py | ||
| :language: python | ||
| :lines: 17-19 | ||
| We chose to add the first three diffraction orders (plus a 0.1 margin to avoid problems | ||
| with floating point comparisons). | ||
| Finally, we set-up the illumination and calculate the scattering with the usual | ||
| procedure. | ||
| .. literalinclude:: examples/chain_tmatrixc.py | ||
| :language: python | ||
| :lines: 21-27 | ||
| We evaluate the fields in two regions. Outside of the circumscribing cylinders we can | ||
| use the fast cylindrical wave expansion. Inside of the circumscribing cylinders but | ||
| outside of the spheres we can use the method of | ||
| :ref:`tmatrix:One-dimensional arrays (along z)`. | ||
| Finally, we can plot the results. To illustrate the periodicity better, three unit cells | ||
| are shown. | ||
| .. plot:: examples/chain_tmatrixc.py | ||
| Clusters | ||
| ======== | ||
| Similarly to the case of spheres we can also calculate the response from a cluster of | ||
| objects. For the example want to simulate a cylinder together with a chain of spheres | ||
| in the cylindrical wave basis as described in the previous section. | ||
| So, we set up first the spheres in the chain and convert them to the cylindrical wave | ||
| basis as before | ||
| .. literalinclude:: examples/cluster_tmatrixc.py | ||
| :language: python | ||
| :lines: 6-18 | ||
| Then, we create the cylinder T-matrix in the cylindrical wave basis | ||
| .. literalinclude:: examples/cluster_tmatrixc.py | ||
| :language: python | ||
| :lines: 20 | ||
| Finally, we construct the cluster and let the interaction be solved | ||
| .. literalinclude:: examples/cluster_tmatrixc.py | ||
| :language: python | ||
| :lines: 22-29 | ||
| and we solve calculate the scattered field coefficients, whose field representation we | ||
| then plot | ||
| .. plot:: examples/cluster_tmatrixc.py | ||
| One-dimensional arrays (along the x-axis) | ||
| ========================================= | ||
| Now, we take the chain of spheres and a cylinder and place them in a grating structure | ||
| along the x-direction. We start again by defining the parameters and calculating the | ||
| relevant cylindrical T-matrices. | ||
| .. literalinclude:: examples/grating_tmatrixc.py | ||
| :language: python | ||
| :lines: 6-21 | ||
| Next, we create the cluster and, as usual, let it interact within a lattice of the | ||
| defined periodicity. Then, it's simple to calculate the scattering coefficients. | ||
| .. literalinclude:: examples/grating_tmatrixc.py | ||
| :language: python | ||
| :lines: 23-32 | ||
| In the last step, we want to sum up the scattered fields at each point in the probing | ||
| area | ||
| .. literalinclude:: examples/grating_tmatrixc.py | ||
| :language: python | ||
| :lines: 34-43 | ||
| and plot the results. | ||
| .. plot:: examples/grating_tmatrixc.py | ||
| Two-dimensional arrays (in the x-y-plane) | ||
| ========================================= | ||
| As last example, we want to examine a structure that is a photonic crystal consisting | ||
| of infinitely long cylinders in a square array in the x-y-plane. | ||
| .. literalinclude:: examples/crystal_tmatrixc.py | ||
| :language: python | ||
| :lines: 6-23 | ||
| Similarly to the case of spheres and a three-dimensional lattice, we can check the | ||
| smallest singular value. | ||
| .. plot:: examples/crystal_tmatrixc.py | ||
| ========= | ||
| Reference | ||
| ========= | ||
| .. automodule:: treams | ||
| :no-members: | ||
| :no-inherited-members: | ||
| :no-special-members: | ||
| Modules | ||
| ======= | ||
| These modules provide basic functionality for transformations within one basis set, i.e. | ||
| one module, like translations and rotations as well as transformations among them. | ||
| The functions in there provide an intermediate stage between the purely mathematical | ||
| functions found in the two subpackages :mod:`lattice` and :mod:`special` and the | ||
| higher-level classes and functions. | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| :template: bare-module | ||
| ~treams.sw | ||
| ~treams.cw | ||
| ~treams.pw | ||
| This module is for loading and storing data in HDF5 files and also for creating meshes | ||
| of sphere ensembles. | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| ~treams.io | ||
| Finally, two modules for calculating scattering coefficients and doing miscellaneous | ||
| tasks. | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| ~treams.coeffs | ||
| ~treams.misc | ||
| Global configuration variables are stored in | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| ~treams.config | ||
| Some convenience classes for the implementation are defined in | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| ~treams.util | ||
| Subpackages | ||
| =========== | ||
| These subpackages allow a low-level access to the implementation of the lattice sums and | ||
| mathematical functions | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| :template: bare-module | ||
| ~treams.lattice | ||
| ~treams.special |
| global-exclude *.pyx *.pxd .gitignore | ||
| include src/treams/lattice/cython_lattice.pxd | ||
| include src/treams/special/cython_special.pxd |
-96
| Metadata-Version: 2.1 | ||
| Name: treams | ||
| Version: 0.4.1 | ||
| Summary: "T-matrix scattering code for nanophotonic computations" | ||
| Home-page: https://github.com/tfp-photonics/treams | ||
| Author: Dominik Beutel | ||
| Author-email: dominik.beutel@kit.edu | ||
| License: MIT | ||
| Platform: Linux | ||
| Platform: Windows | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: Intended Audience :: Science/Research | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Operating System :: POSIX :: Linux | ||
| Classifier: Natural Language :: English | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Cython | ||
| Classifier: Programming Language :: Python :: Implementation :: CPython | ||
| Classifier: Topic :: Scientific/Engineering | ||
| Classifier: Topic :: Scientific/Engineering :: Astronomy | ||
| Classifier: Topic :: Scientific/Engineering :: Atmospheric Science | ||
| Classifier: Topic :: Scientific/Engineering :: Physics | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE | ||
| Requires-Dist: numpy | ||
| Requires-Dist: scipy>=1.6 | ||
| Provides-Extra: coverage | ||
| Requires-Dist: Cython; extra == "coverage" | ||
| Requires-Dist: pytest-cov; extra == "coverage" | ||
| Provides-Extra: docs | ||
| Requires-Dist: matplotlib; extra == "docs" | ||
| Requires-Dist: sphinx; extra == "docs" | ||
| Provides-Extra: io | ||
| Requires-Dist: h5py; extra == "io" | ||
| Provides-Extra: test | ||
| Requires-Dist: pytest; extra == "test" | ||
|  | ||
| [](https://pypi.org/project/treams) | ||
|  | ||
|  | ||
| [](https://tfp-photonics.github.io/treams) | ||
|  | ||
|  | ||
| [](https://htmlpreview.github.io/?https://github.com/tfp-photonics/treams/blob/htmlcov/index.html) | ||
| # treams | ||
| The package `treams` provides a framework to simplify computations of the | ||
| electromagnetic scattering of waves at finite and at periodic arrangements of particles | ||
| based on the T-matrix method. | ||
| ## Installation | ||
| ### Installation using pip | ||
| To install the package with pip, use | ||
| ```sh | ||
| pip install treams | ||
| ``` | ||
| If you're using the system wide installed version of python, you might consider the | ||
| ``--user`` option. | ||
| ## Documentation | ||
| The documentation can be found at https://tfp-photonics.github.io/treams. | ||
| ## Publications | ||
| When using this code please cite: | ||
| [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, treams - A T-matrix scattering code for nanophotonic computations, arXiv (preprint), 2309.03182 (2023).](https://doi.org/10.48550/arXiv.2309.03182) | ||
| Other relevant publications are | ||
| * [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, Unified Lattice Sums Accommodating Multiple Sublattices for Solutions of the Helmholtz Equation in Two and Three Dimensions, Phys. Rev. A 107, 013508 (2023).](https://doi.org/10.1103/PhysRevA.107.013508) | ||
| * [D. Beutel, P. Scott, M. Wegener, C. Rockstuhl, and I. Fernandez-Corbaton, Enhancing the Optical Rotation of Chiral Molecules Using Helicity Preserving All-Dielectric Metasurfaces, Appl. Phys. Lett. 118, 221108 (2021).](https://doi.org/10.1063/5.0050411) | ||
| * [D. Beutel, A. Groner, C. Rockstuhl, C. Rockstuhl, and I. Fernandez-Corbaton, Efficient Simulation of Biperiodic, Layered Structures Based on the T-Matrix Method, J. Opt. Soc. Am. B, JOSAB 38, 1782 (2021).](https://doi.org/10.1364/JOSAB.419645) | ||
| ## Features | ||
| * [x] T-matrix calculations using a spherical or cylindrical wave basis set | ||
| * [x] Calculations in helicity and parity (TE/TM) basis | ||
| * [x] Scattering from clusters of particles | ||
| * [x] Scattering from particles and clusters arranged in 3d-, 2d-, and 1d-lattices | ||
| * [x] Calculation of light propagation in stratified media | ||
| * [x] Band calculation in crystal structures |
| [build-system] | ||
| requires = [ | ||
| "setuptools", | ||
| "wheel", | ||
| "Cython", | ||
| "oldest-supported-numpy", | ||
| "scipy>=1.6", | ||
| "setuptools_scm>=6.2" | ||
| ] | ||
| build-backend = "setuptools.build_meta" | ||
| [tool.isort] | ||
| profile = "black" | ||
| [tool.setuptools_scm] | ||
| [tool.pylint.messages_control] | ||
| extension-pkg-whitelist = "treams" | ||
| [tool.cibuildwheel] | ||
| archs = ["auto64"] | ||
| skip = ["pp*", "*musllinux*"] | ||
| test-command = "python -m pytest {project}/tests/unit" | ||
| test-extras = ["test", "io"] |
-52
|  | ||
| [](https://pypi.org/project/treams) | ||
|  | ||
|  | ||
| [](https://tfp-photonics.github.io/treams) | ||
|  | ||
|  | ||
| [](https://htmlpreview.github.io/?https://github.com/tfp-photonics/treams/blob/htmlcov/index.html) | ||
| # treams | ||
| The package `treams` provides a framework to simplify computations of the | ||
| electromagnetic scattering of waves at finite and at periodic arrangements of particles | ||
| based on the T-matrix method. | ||
| ## Installation | ||
| ### Installation using pip | ||
| To install the package with pip, use | ||
| ```sh | ||
| pip install treams | ||
| ``` | ||
| If you're using the system wide installed version of python, you might consider the | ||
| ``--user`` option. | ||
| ## Documentation | ||
| The documentation can be found at https://tfp-photonics.github.io/treams. | ||
| ## Publications | ||
| When using this code please cite: | ||
| [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, treams - A T-matrix scattering code for nanophotonic computations, arXiv (preprint), 2309.03182 (2023).](https://doi.org/10.48550/arXiv.2309.03182) | ||
| Other relevant publications are | ||
| * [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, Unified Lattice Sums Accommodating Multiple Sublattices for Solutions of the Helmholtz Equation in Two and Three Dimensions, Phys. Rev. A 107, 013508 (2023).](https://doi.org/10.1103/PhysRevA.107.013508) | ||
| * [D. Beutel, P. Scott, M. Wegener, C. Rockstuhl, and I. Fernandez-Corbaton, Enhancing the Optical Rotation of Chiral Molecules Using Helicity Preserving All-Dielectric Metasurfaces, Appl. Phys. Lett. 118, 221108 (2021).](https://doi.org/10.1063/5.0050411) | ||
| * [D. Beutel, A. Groner, C. Rockstuhl, C. Rockstuhl, and I. Fernandez-Corbaton, Efficient Simulation of Biperiodic, Layered Structures Based on the T-Matrix Method, J. Opt. Soc. Am. B, JOSAB 38, 1782 (2021).](https://doi.org/10.1364/JOSAB.419645) | ||
| ## Features | ||
| * [x] T-matrix calculations using a spherical or cylindrical wave basis set | ||
| * [x] Calculations in helicity and parity (TE/TM) basis | ||
| * [x] Scattering from clusters of particles | ||
| * [x] Scattering from particles and clusters arranged in 3d-, 2d-, and 1d-lattices | ||
| * [x] Calculation of light propagation in stratified media | ||
| * [x] Band calculation in crystal structures |
-73
| [metadata] | ||
| name = treams | ||
| author = Dominik Beutel | ||
| author_email = dominik.beutel@kit.edu | ||
| url = https://github.com/tfp-photonics/treams | ||
| description = "T-matrix scattering code for nanophotonic computations" | ||
| license = MIT | ||
| long_description = file: README.md | ||
| long_description_content_type = text/markdown | ||
| platform = Linux, Windows | ||
| classifiers = | ||
| Development Status :: 4 - Beta | ||
| Intended Audience :: Science/Research | ||
| Operating System :: Microsoft :: Windows | ||
| Operating System :: POSIX :: Linux | ||
| Natural Language :: English | ||
| Programming Language :: Python | ||
| Programming Language :: Python :: 3 | ||
| Programming Language :: Python :: 3 :: Only | ||
| Programming Language :: Python :: 3.8 | ||
| Programming Language :: Python :: 3.9 | ||
| Programming Language :: Python :: 3.10 | ||
| Programming Language :: Python :: 3.11 | ||
| Programming Language :: Cython | ||
| Programming Language :: Python :: Implementation :: CPython | ||
| Topic :: Scientific/Engineering | ||
| Topic :: Scientific/Engineering :: Astronomy | ||
| Topic :: Scientific/Engineering :: Atmospheric Science | ||
| Topic :: Scientific/Engineering :: Physics | ||
| [options] | ||
| python_requires = >= 3.8 | ||
| package_dir = | ||
| =src | ||
| packages = | ||
| treams | ||
| treams.special | ||
| treams.lattice | ||
| install_requires = | ||
| numpy | ||
| scipy >= 1.6 | ||
| [options.extras_require] | ||
| coverage = | ||
| Cython | ||
| pytest-cov | ||
| docs = | ||
| matplotlib | ||
| sphinx | ||
| io = | ||
| h5py | ||
| test = | ||
| pytest | ||
| [sdist] | ||
| formats = zip, gztar | ||
| [pydocstyle] | ||
| inherit = false | ||
| convention = google | ||
| [flake8] | ||
| filename = *.py,*.pyx,*.pxd | ||
| max-line-length = 88 | ||
| extend-ignore = E203, E501 | ||
| per-file-ignores = | ||
| *.pyx:E211,E225,E227 | ||
| *.pxd:E211,E225,E227 | ||
| [egg_info] | ||
| tag_build = | ||
| tag_date = 0 | ||
-104
| """Packaging of treams.""" | ||
| import os | ||
| import numpy as np | ||
| from setuptools import Extension, setup | ||
| from setuptools.command.build_ext import build_ext as _build_ext | ||
| try: | ||
| from Cython.Build import cythonize | ||
| except ImportError: | ||
| cythonize = None | ||
| if os.name == "nt": | ||
| link_args = [ | ||
| "-static-libgcc", | ||
| "-static-libstdc++", | ||
| "-Wl,-Bstatic,--whole-archive", | ||
| "-lwinpthread", | ||
| "-Wl,--no-whole-archive", | ||
| ] | ||
| compile_args = ["-DMS_WIN64"] | ||
| class build_ext(_build_ext): | ||
| """build_ext for Windows.""" | ||
| def finalize_options(self): | ||
| """Set compiler to gcc.""" | ||
| super().finalize_options() | ||
| self.compiler = "mingw32" | ||
| # https://cython.readthedocs.io/en/latest/src/tutorial/appendix.html | ||
| def build_extensions(self): | ||
| """Add Windows specific compiler and linker arguments.""" | ||
| if self.compiler.compiler_type == "mingw32": | ||
| for e in self.extensions: | ||
| e.extra_compile_args = compile_args | ||
| e.extra_link_args = link_args | ||
| super().build_extensions() | ||
| else: | ||
| build_ext = _build_ext | ||
| # https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules | ||
| def no_cythonize(extensions, **_ignore): | ||
| """Add c and c++ code to source archive.""" | ||
| for extension in extensions: | ||
| sources = [] | ||
| for sfile in extension.sources: | ||
| path, ext = os.path.splitext(sfile) | ||
| if ext in (".pyx", ".py"): | ||
| if extension.language == "c++": | ||
| ext = ".cpp" | ||
| else: | ||
| ext = ".c" | ||
| sfile = path + ext | ||
| sources.append(sfile) | ||
| extension.sources[:] = sources | ||
| return extensions | ||
| keys = {"include_dirs": [np.get_include()]} | ||
| compiler_directives = {"language_level": "3"} | ||
| if os.environ.get("CYTHON_COVERAGE", False): | ||
| keys["define_macros"] = [("CYTHON_TRACE_NOGIL", "1")] | ||
| compiler_directives["linetrace"] = True | ||
| extension_names = [ | ||
| "treams.coeffs", | ||
| "treams.config", | ||
| "treams.cw", | ||
| "treams.pw", | ||
| "treams.sw", | ||
| "treams.lattice._dsum", | ||
| "treams.lattice._esum", | ||
| "treams.lattice._gufuncs", | ||
| "treams.lattice._misc", | ||
| "treams.lattice.cython_lattice", | ||
| "treams.special._bessel", | ||
| "treams.special._coord", | ||
| "treams.special._gufuncs", | ||
| "treams.special._integrals", | ||
| "treams.special._misc", | ||
| "treams.special._ufuncs", | ||
| "treams.special._waves", | ||
| "treams.special._wigner3j", | ||
| "treams.special._wignerd", | ||
| "treams.special.cython_special", | ||
| ] | ||
| extensions = [ | ||
| Extension(name, [f"src/{name.replace('.', '/')}.pyx"], **keys) | ||
| for name in extension_names | ||
| ] | ||
| if cythonize is not None: | ||
| try: | ||
| extensions = cythonize(extensions, compiler_directives=compiler_directives) | ||
| except ValueError: | ||
| extensions = no_cythonize(extensions) | ||
| else: | ||
| extensions = no_cythonize(extensions) | ||
| setup(ext_modules=extensions, cmdclass={"build_ext": build_ext}) |
| Metadata-Version: 2.1 | ||
| Name: treams | ||
| Version: 0.4.1 | ||
| Summary: "T-matrix scattering code for nanophotonic computations" | ||
| Home-page: https://github.com/tfp-photonics/treams | ||
| Author: Dominik Beutel | ||
| Author-email: dominik.beutel@kit.edu | ||
| License: MIT | ||
| Platform: Linux | ||
| Platform: Windows | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: Intended Audience :: Science/Research | ||
| Classifier: Operating System :: Microsoft :: Windows | ||
| Classifier: Operating System :: POSIX :: Linux | ||
| Classifier: Natural Language :: English | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Cython | ||
| Classifier: Programming Language :: Python :: Implementation :: CPython | ||
| Classifier: Topic :: Scientific/Engineering | ||
| Classifier: Topic :: Scientific/Engineering :: Astronomy | ||
| Classifier: Topic :: Scientific/Engineering :: Atmospheric Science | ||
| Classifier: Topic :: Scientific/Engineering :: Physics | ||
| Requires-Python: >=3.8 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE | ||
| Requires-Dist: numpy | ||
| Requires-Dist: scipy>=1.6 | ||
| Provides-Extra: coverage | ||
| Requires-Dist: Cython; extra == "coverage" | ||
| Requires-Dist: pytest-cov; extra == "coverage" | ||
| Provides-Extra: docs | ||
| Requires-Dist: matplotlib; extra == "docs" | ||
| Requires-Dist: sphinx; extra == "docs" | ||
| Provides-Extra: io | ||
| Requires-Dist: h5py; extra == "io" | ||
| Provides-Extra: test | ||
| Requires-Dist: pytest; extra == "test" | ||
|  | ||
| [](https://pypi.org/project/treams) | ||
|  | ||
|  | ||
| [](https://tfp-photonics.github.io/treams) | ||
|  | ||
|  | ||
| [](https://htmlpreview.github.io/?https://github.com/tfp-photonics/treams/blob/htmlcov/index.html) | ||
| # treams | ||
| The package `treams` provides a framework to simplify computations of the | ||
| electromagnetic scattering of waves at finite and at periodic arrangements of particles | ||
| based on the T-matrix method. | ||
| ## Installation | ||
| ### Installation using pip | ||
| To install the package with pip, use | ||
| ```sh | ||
| pip install treams | ||
| ``` | ||
| If you're using the system wide installed version of python, you might consider the | ||
| ``--user`` option. | ||
| ## Documentation | ||
| The documentation can be found at https://tfp-photonics.github.io/treams. | ||
| ## Publications | ||
| When using this code please cite: | ||
| [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, treams - A T-matrix scattering code for nanophotonic computations, arXiv (preprint), 2309.03182 (2023).](https://doi.org/10.48550/arXiv.2309.03182) | ||
| Other relevant publications are | ||
| * [D. Beutel, I. Fernandez-Corbaton, and C. Rockstuhl, Unified Lattice Sums Accommodating Multiple Sublattices for Solutions of the Helmholtz Equation in Two and Three Dimensions, Phys. Rev. A 107, 013508 (2023).](https://doi.org/10.1103/PhysRevA.107.013508) | ||
| * [D. Beutel, P. Scott, M. Wegener, C. Rockstuhl, and I. Fernandez-Corbaton, Enhancing the Optical Rotation of Chiral Molecules Using Helicity Preserving All-Dielectric Metasurfaces, Appl. Phys. Lett. 118, 221108 (2021).](https://doi.org/10.1063/5.0050411) | ||
| * [D. Beutel, A. Groner, C. Rockstuhl, C. Rockstuhl, and I. Fernandez-Corbaton, Efficient Simulation of Biperiodic, Layered Structures Based on the T-Matrix Method, J. Opt. Soc. Am. B, JOSAB 38, 1782 (2021).](https://doi.org/10.1364/JOSAB.419645) | ||
| ## Features | ||
| * [x] T-matrix calculations using a spherical or cylindrical wave basis set | ||
| * [x] Calculations in helicity and parity (TE/TM) basis | ||
| * [x] Scattering from clusters of particles | ||
| * [x] Scattering from particles and clusters arranged in 3d-, 2d-, and 1d-lattices | ||
| * [x] Calculation of light propagation in stratified media | ||
| * [x] Band calculation in crystal structures |
| numpy | ||
| scipy>=1.6 | ||
| [coverage] | ||
| Cython | ||
| pytest-cov | ||
| [docs] | ||
| matplotlib | ||
| sphinx | ||
| [io] | ||
| h5py | ||
| [test] | ||
| pytest |
| .coveragerc | ||
| LICENSE | ||
| MANIFEST.in | ||
| README.md | ||
| conftest.py | ||
| pyproject.toml | ||
| setup.cfg | ||
| setup.py | ||
| .github/workflows/build.yml | ||
| .github/workflows/docs.yml | ||
| .github/workflows/doctests.yml | ||
| .github/workflows/tests.yml | ||
| docs/Makefile | ||
| docs/about.rst | ||
| docs/conf.py | ||
| docs/dev.rst | ||
| docs/gettingstarted.rst | ||
| docs/index.rst | ||
| docs/intro.rst | ||
| docs/make.bat | ||
| docs/maxwell.rst | ||
| docs/operators.rst | ||
| docs/params.rst | ||
| docs/physicsarray.rst | ||
| docs/smatrix.rst | ||
| docs/theory.rst | ||
| docs/tmatrix.rst | ||
| docs/tmatrixc.rst | ||
| docs/treams.rst | ||
| docs/_static/custom.css | ||
| docs/_templates/autosummary/attribute.rst | ||
| docs/_templates/autosummary/bare-module.rst | ||
| docs/_templates/autosummary/class.rst | ||
| docs/_templates/autosummary/cython-module.rst | ||
| docs/_templates/autosummary/module.rst | ||
| docs/examples/array_spheres.py | ||
| docs/examples/array_spheres_tmatrixc.py | ||
| docs/examples/band_structure.py | ||
| docs/examples/chain.py | ||
| docs/examples/chain_tmatrixc.py | ||
| docs/examples/cluster.py | ||
| docs/examples/cluster_tmatrixc.py | ||
| docs/examples/crystal.py | ||
| docs/examples/crystal_tmatrixc.py | ||
| docs/examples/cylinder_tmatrixc.py | ||
| docs/examples/grating_tmatrixc.py | ||
| docs/examples/grid.py | ||
| docs/examples/slab.py | ||
| docs/examples/sphere.py | ||
| src/treams/__init__.py | ||
| src/treams/_core.py | ||
| src/treams/_lattice.py | ||
| src/treams/_material.py | ||
| src/treams/_operators.py | ||
| src/treams/_smatrix.py | ||
| src/treams/_tmatrix.py | ||
| src/treams/coeffs.c | ||
| src/treams/config.c | ||
| src/treams/cw.c | ||
| src/treams/ebcm.py | ||
| src/treams/io.py | ||
| src/treams/misc.py | ||
| src/treams/pw.c | ||
| src/treams/sw.c | ||
| src/treams/util.py | ||
| src/treams.egg-info/PKG-INFO | ||
| src/treams.egg-info/SOURCES.txt | ||
| src/treams.egg-info/dependency_links.txt | ||
| src/treams.egg-info/requires.txt | ||
| src/treams.egg-info/top_level.txt | ||
| src/treams/lattice/__init__.py | ||
| src/treams/lattice/_dsum.c | ||
| src/treams/lattice/_esum.c | ||
| src/treams/lattice/_gufuncs.c | ||
| src/treams/lattice/_misc.c | ||
| src/treams/lattice/cython_lattice.c | ||
| src/treams/lattice/cython_lattice.pxd | ||
| src/treams/special/__init__.py | ||
| src/treams/special/_bessel.c | ||
| src/treams/special/_coord.c | ||
| src/treams/special/_gufuncs.c | ||
| src/treams/special/_integrals.c | ||
| src/treams/special/_misc.c | ||
| src/treams/special/_ufuncs.c | ||
| src/treams/special/_waves.c | ||
| src/treams/special/_wigner3j.c | ||
| src/treams/special/_wignerd.c | ||
| src/treams/special/cython_special.c | ||
| src/treams/special/cython_special.pxd | ||
| tests/integration/test_compare_direct_sum.py | ||
| tests/unit/test_coeffs.py | ||
| tests/unit/test_core.py | ||
| tests/unit/test_cw.py | ||
| tests/unit/test_io.py | ||
| tests/unit/test_lattice.py | ||
| tests/unit/test_lattice_module.py | ||
| tests/unit/test_misc.py | ||
| tests/unit/test_operators.py | ||
| tests/unit/test_pw.py | ||
| tests/unit/test_smatrix.py | ||
| tests/unit/test_special.py | ||
| tests/unit/test_sw.py | ||
| tests/unit/test_tmatrix.py | ||
| tests/unit/test_tmatrixc.py | ||
| tests/unit/test_util.py |
| """TREAMS: T-Matrix scattering code for nanophotonic computations. | ||
| .. currentmodule:: treams | ||
| Classes | ||
| ======= | ||
| The top-level classes and functions allow a high-level access to the functionality. | ||
| Basis sets | ||
| ---------- | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| CylindricalWaveBasis | ||
| PlaneWaveBasisByUnitVector | ||
| PlaneWaveBasisByComp | ||
| SphericalWaveBasis | ||
| Matrices and Arrays | ||
| ------------------- | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| PhysicsArray | ||
| SMatrix | ||
| SMatrices | ||
| TMatrix | ||
| TMatrixC | ||
| Other | ||
| ----- | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| Lattice | ||
| Material | ||
| Functions | ||
| ========= | ||
| .. autosummary:: | ||
| :toctree: generated/ | ||
| bfield | ||
| changepoltype | ||
| chirality_density | ||
| dfield | ||
| efield | ||
| expand | ||
| expandlattice | ||
| hfield | ||
| permute | ||
| plane_wave | ||
| rotate | ||
| translate | ||
| """ | ||
| from treams._core import ( # noqa: F401 | ||
| CylindricalWaveBasis, | ||
| PhysicsArray, | ||
| PlaneWaveBasisByComp, | ||
| PlaneWaveBasisByUnitVector, | ||
| SphericalWaveBasis, | ||
| ) | ||
| from treams._lattice import Lattice, WaveVector # noqa: F401 | ||
| from treams._material import Material # noqa: F401 | ||
| from treams._operators import ( # noqa: F401 | ||
| BField, | ||
| ChangePoltype, | ||
| DField, | ||
| EField, | ||
| Expand, | ||
| ExpandLattice, | ||
| FField, | ||
| GField, | ||
| HField, | ||
| Permute, | ||
| Rotate, | ||
| Translate, | ||
| bfield, | ||
| changepoltype, | ||
| dfield, | ||
| efield, | ||
| expand, | ||
| expandlattice, | ||
| ffield, | ||
| gfield, | ||
| hfield, | ||
| permute, | ||
| rotate, | ||
| translate, | ||
| ) | ||
| from treams._smatrix import ( # noqa: F401 | ||
| SMatrices, | ||
| SMatrix, | ||
| chirality_density, | ||
| poynting_avg_z, | ||
| ) | ||
| from treams._tmatrix import ( # noqa: F401 | ||
| TMatrix, | ||
| TMatrixC, | ||
| cylindrical_wave, | ||
| plane_wave, | ||
| plane_wave_angle, | ||
| spherical_wave, | ||
| ) |
-1297
| """Basis sets and core array functionalities.""" | ||
| import abc | ||
| from collections import namedtuple | ||
| import numpy as np | ||
| import treams._operators as op | ||
| import treams.lattice as la | ||
| from treams import util | ||
| from treams._lattice import Lattice, WaveVector | ||
| from treams._material import Material | ||
| class BasisSet(util.OrderedSet, metaclass=abc.ABCMeta): | ||
| """Basis set base class. | ||
| It is the base class for all basis sets used. They are expected to be an ordered | ||
| sequence of the modes, that are included in a expansion. Basis sets are expected to | ||
| be immutable. | ||
| """ | ||
| _names = () | ||
| """Names of the relevant parameters""" | ||
| def __repr__(self): | ||
| """String representation. | ||
| Automatically generated when the attribute ``_names`` is defined. | ||
| Returns: | ||
| str | ||
| """ | ||
| string = ",\n ".join(f"{name}={i}" for name, i in zip(self._names, self[()])) | ||
| return f"{self.__class__.__name__}(\n {string},\n)" | ||
| def __len__(self): | ||
| """Number of modes.""" | ||
| return len(self.pol) | ||
| @classmethod | ||
| @abc.abstractmethod | ||
| def default(cls, *args, **kwargs): | ||
| """Construct a default basis from parameters. | ||
| Construct a basis set in a default order by giving few parameters. | ||
| """ | ||
| raise NotImplementedError | ||
| class SphericalWaveBasis(BasisSet): | ||
| r"""Basis of spherical waves. | ||
| Functions of the spherical wave basis are defined by their angular momentum ``l``, | ||
| its projection onto the z-axis ``m``, and the polarization ``pol``. If the basis | ||
| is defined with respect to a single origin it is referred to as "global", if it | ||
| contains multiple origins it is referred to as "local". In a local basis an | ||
| additional position index ``pidx`` is used to link the modes to one of the | ||
| specified ``positions``. | ||
| For spherical waves there exist multiple :ref:`params:Polarizations` and they | ||
| can be separated into incident and scattered fields. Depending on these combinations | ||
| the basis modes refer to one of the functions :func:`~treams.special.vsw_A`, | ||
| :func:`~treams.special.vsw_rA`, :func:`~treams.special.vsw_M`, | ||
| :func:`~treams.special.vsw_rM`, :func:`~treams.special.vsw_N`, or | ||
| :func:`~treams.special.vsw_rN`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``l``, ``m``, and | ||
| ``pol`` or ``pidx``, ``l``, ``m``, and``pol``. | ||
| positions (array-like, optional): The positions of the origins for the specified | ||
| modes. Defaults to ``[[0, 0, 0]]``. | ||
| Attributes: | ||
| pidx (array-like): Integer referring to a row in :attr:`positions`. | ||
| l (array-like): Angular momentum as an integer :math:`l > 0` | ||
| m (array-like): Angular momentum projection onto the z-axis, it is an integer | ||
| with :math:`m \leq |l|` | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| _names = ("pidx", "l", "m", "pol") | ||
| def __init__(self, modes, positions=None): | ||
| """Initalization.""" | ||
| tmp = [] | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| pidx = [] | ||
| l = [] # noqa: E741 | ||
| m = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 3: | ||
| l, m, pol = (*zip(*modes),) | ||
| pidx = np.zeros_like(l) | ||
| elif len(modes[0]) == 4: | ||
| pidx, l, m, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| if positions is None: | ||
| positions = np.zeros((1, 3)) | ||
| positions = np.array(positions, float) | ||
| if positions.ndim == 1: | ||
| positions = positions[None, :] | ||
| if positions.ndim != 2 or positions.shape[1] != 3: | ||
| raise ValueError(f"invalid shape of positions {positions.shape}") | ||
| self.pidx, self.l, self.m, self.pol = [ | ||
| np.array(i, int) for i in (pidx, l, m, pol) | ||
| ] | ||
| for i, j in ((self.pidx, pidx), (self.l, l), (self.m, m), (self.pol, pol)): | ||
| i.flags.writeable = False | ||
| if np.any(i != j): | ||
| raise ValueError("parameters must be integer") | ||
| if np.any(self.l < 1): | ||
| raise ValueError("'l' must be a strictly positive integer") | ||
| if np.any(self.l < np.abs(self.m)): | ||
| raise ValueError("'|m|' cannot be larger than 'l'") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| if np.any(self.pidx >= len(positions)): | ||
| raise ValueError("undefined position is indexed") | ||
| self._positions = positions | ||
| self._positions.flags.writeable = False | ||
| self.lattice = self.kpar = None | ||
| @property | ||
| def positions(self): | ||
| """Positions of the modes' origins. | ||
| The positions are an immutable (N, 3)-array. Each row corresponds to a point in | ||
| the three-dimensional Cartesian space. | ||
| """ | ||
| return self._positions | ||
| def __repr__(self): | ||
| """String representation.""" | ||
| positions = "positions=" + str(self.positions).replace("\n", ",") | ||
| return f"{super().__repr__()[:-1]} {positions},\n)" | ||
| @property | ||
| def isglobal(self): | ||
| """Basis is defined with respect to a single (global) origin. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return len(self) == 0 or np.all(self.pidx == self.pidx[0]) | ||
| def __getattr__(self, key): | ||
| dct = {"l": "l", "m": "m", "p": "pidx", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| Alternatively, the string "plmp", "lmp", or "lm" can be used to access only a | ||
| subset of :attr:`pidx`, :attr:`l`, :attr:`m`, and :attr:`pol`. | ||
| """ | ||
| res = self.pidx[idx], self.l[idx], self.m[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or ( | ||
| isinstance(idx, tuple) and len(idx) == 0 | ||
| ): | ||
| return res | ||
| return type(self)(zip(*res), self.positions) | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same order | ||
| and the specified origin :attr:`positions` are equal. | ||
| """ | ||
| try: | ||
| return self is other or ( | ||
| np.array_equal(self.pidx, other.pidx) | ||
| and np.array_equal(self.l, other.l) | ||
| and np.array_equal(self.m, other.m) | ||
| and np.array_equal(self.pol, other.pol) | ||
| and np.array_equal(self.positions, other.positions) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| @classmethod | ||
| def default(cls, lmax, nmax=1, positions=None): | ||
| """Default basis for the given maximal multipolar order. | ||
| The default order contains separate blocks for each position index which are in | ||
| ascending order. Within each block the modes are sorted by angular momentum | ||
| :math:`l`, with the lowest angular momentum coming first. For each angular | ||
| momentum its z-projection is in ascending order from :math:`m = -l` to | ||
| :math:`m = l`. Finally, the polarization index is the fastest changing index | ||
| which iterates between 1 and 0. | ||
| Example: | ||
| >>> SphericalWaveBasis.default(2) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| l=[1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2], | ||
| m=[-1 -1 0 0 1 1 -2 -2 -1 -1 0 0 1 1 2 2], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| >>> SphericalWaveBasis.default(1, 2, [[0, 0, 1.], [0, 0, -1.]]) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 1 1 1 1 1 1], | ||
| l=[1 1 1 1 1 1 1 1 1 1 1 1], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[ 0. 0. 1.], [ 0. 0. -1.]], | ||
| ) | ||
| Args: | ||
| lmax (int): Maximal multipolar order. | ||
| nmax (int, optional): Number of positions, defaults to 1. | ||
| positions (array-like, optional): Positions of the origins. | ||
| """ | ||
| modes = [ | ||
| [n, l, m, p] | ||
| for n in range(0, nmax) | ||
| for l in range(1, lmax + 1) # noqa: E741 | ||
| for m in range(-l, l + 1) | ||
| for p in range(1, -1, -1) | ||
| ] | ||
| return cls(modes, positions=positions) | ||
| @classmethod | ||
| def ebcm(cls, lmax, nmax=1, mmax=-1, positions=None): | ||
| """Order of modes suited for ECBM. | ||
| In comparison to :meth:`default` this order prioritises blocks of the | ||
| z-projection of the angular momentum :math:`m` over the angular momentum | ||
| :math:`l`. This is useful to get block-diagonal matrices for the extended | ||
| boundary condition method (EBCM). | ||
| Example: | ||
| >>> SphericalWaveBasis.ebcm(2) | ||
| SphericalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| l=[2 2 1 1 2 2 1 1 2 2 1 1 2 2 2 2], | ||
| m=[-2 -2 -1 -1 -1 -1 0 0 0 0 1 1 1 1 2 2], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| Args: | ||
| lmax (int): Maximal multipolar order. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| mmax (int, optional): Maximal value of `|m|`. If ommitted or set to -1 it | ||
| is taken equal to `lmax`. | ||
| positions (array-like, optional): Positions of the origins. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| mmax = lmax if mmax == -1 else mmax | ||
| modes = [ | ||
| [n, l, m, p] | ||
| for n in range(0, nmax) | ||
| for m in range(-mmax, mmax + 1) | ||
| for l in range(max(abs(m), 1), lmax + 1) # noqa: E741 | ||
| for p in range(1, -1, -1) | ||
| ] | ||
| return cls(modes, positions=positions) | ||
| @staticmethod | ||
| def defaultlmax(dim, nmax=1): | ||
| """Calculate the default mode order for a given length. | ||
| Given the dimension of the T-matrix return the estimated maximal value of `l`. | ||
| This is the inverse of :meth:`defaultdim`. A value of zero is allowed for empty | ||
| T-matrices. | ||
| Example: | ||
| >>> SphericalWaveBasis.defaultlmax(len(SphericalWaveBasis.default(3))) | ||
| 3 | ||
| Args: | ||
| dim (int): Dimension of the T-matrix, respectively number of modes. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| Returns: | ||
| int | ||
| """ | ||
| res = np.sqrt(1 + dim * 0.5 / nmax) - 1 | ||
| res_int = int(np.rint(res)) | ||
| if np.abs(res - res_int) > 1e-8 * np.maximum(np.abs(res), np.abs(res_int)): | ||
| raise ValueError("cannot estimate the default lmax") | ||
| return res_int | ||
| @staticmethod | ||
| def defaultdim(lmax, nmax=1): | ||
| """Default number of modes for a given mulipolar order. | ||
| Given the maximal value of `l` return the size of the corresponding T-matrix. | ||
| This is the inverse of :meth:`defaultlmax`. A value of zero is allowed. | ||
| Args: | ||
| lmax (int): Maximal multipolar order | ||
| nmax (int, optional): Number of particles, defaults to `1` | ||
| Returns: | ||
| int | ||
| """ | ||
| # zero is allowed and won't give an error | ||
| if lmax < 0 or nmax < 0: | ||
| raise ValueError("maximal order must be positive") | ||
| return 2 * lmax * (lmax + 2) * nmax | ||
| @classmethod | ||
| def _from_iterable(cls, it, positions=None): | ||
| if isinstance(cls, SphericalWaveBasis): | ||
| positions = cls.positions if positions is None else positions | ||
| cls = type(cls) | ||
| obj = cls(it, positions=positions) | ||
| return obj | ||
| class CylindricalWaveBasis(BasisSet): | ||
| r"""Basis of cylindrical waves. | ||
| Functions of the cylindrical wave basis are defined by the z-components of the wave | ||
| vector ``kz`` and the angular momentum ``m`` as well as the polarization ``pol``. | ||
| If the basis is defined with respect to a single origin it is referred to as | ||
| "global", if it contains multiple origins it is referred to as "local". In a local | ||
| basis an additional position index ``pidx`` is used to link the modes to one of the | ||
| specified ``positions``. | ||
| For cylindrical waves there exist multiple :ref:`params:Polarizations` and | ||
| they can be separated into incident and scattered fields. Depending on these | ||
| combinations the basis modes refer to one of the functions | ||
| :func:`~treams.special.vcw_A`, :func:`~treams.special.vcw_rA`, | ||
| :func:`~treams.special.vcw_M`, :func:`~treams.special.vcw_rM`, | ||
| :func:`~treams.special.vcw_N`, or :func:`~treams.special.vcw_rN`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``kz``, ``m``, and | ||
| ``pol`` or ``pidx``, ``kz``, ``m``, and``pol``. | ||
| positions (array-like, optional): The positions of the origins for the specified | ||
| modes. Defaults to ``[[0, 0, 0]]``. | ||
| Attributes: | ||
| pidx (array-like): Integer referring to a row in :attr:`positions`. | ||
| kz (array-like): Real valued z-component of the wave vector. | ||
| m (array-like): Integer angular momentum projection onto the z-axis. | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| _names = ("pidx", "kz", "m", "pol") | ||
| def __init__(self, modes, positions=None): | ||
| """Initalization.""" | ||
| tmp = [] | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| pidx = [] | ||
| kz = [] | ||
| m = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 3: | ||
| kz, m, pol = (*zip(*modes),) | ||
| pidx = np.zeros_like(m) | ||
| elif len(modes[0]) == 4: | ||
| pidx, kz, m, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| if positions is None: | ||
| positions = np.zeros((1, 3)) | ||
| positions = np.array(positions, float) | ||
| if positions.ndim == 1: | ||
| positions = positions[None, :] | ||
| if positions.ndim != 2 or positions.shape[1] != 3: | ||
| raise ValueError(f"invalid shape of positions {positions.shape}") | ||
| self.pidx, self.m, self.pol = [np.array(i, int) for i in (pidx, m, pol)] | ||
| self.kz = np.array(kz, float) | ||
| self.kz.flags.writeable = False | ||
| for i, j in ((self.pidx, pidx), (self.m, m), (self.pol, pol)): | ||
| i.flags.writeable = False | ||
| if np.any(i != j): | ||
| raise ValueError("parameters must be integer") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| if np.any(self.pidx >= len(positions)): | ||
| raise ValueError("undefined position is indexed") | ||
| self._positions = positions | ||
| self._positions.flags.writeable = False | ||
| self.lattice = self.kpar = None | ||
| if len(self.kz) > 0 and np.all(self.kz == self.kz[0]): | ||
| self.kpar = WaveVector(self.kz[0]) | ||
| @property | ||
| def positions(self): | ||
| """Positions of the modes' origins. | ||
| The positions are an immutable (N, 3)-array. Each row corresponds to a point in | ||
| the three-dimensional Cartesian space. | ||
| """ | ||
| return self._positions | ||
| def __repr__(self): | ||
| """String representation.""" | ||
| positions = "positions=" + str(self.positions).replace("\n", ",") | ||
| return f"{super().__repr__()[:-1]} {positions},\n)" | ||
| @property | ||
| def isglobal(self): | ||
| """Basis is defined with respect to a single (global) origin. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return len(self) == 0 or np.all(self.pidx == self.pidx[0]) | ||
| def __getattr__(self, key): | ||
| dct = {"z": "kz", "m": "m", "p": "pidx", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| Alternatively, the string "pkzmp", "kzmp", or "kzm" can be used to access only a | ||
| subset of :attr:`pidx`, :attr:`kz`, :attr:`m`, and :attr:`pol`. | ||
| """ | ||
| res = self.pidx[idx], self.kz[idx], self.m[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or (isinstance(idx, tuple) and idx == ()): | ||
| return res | ||
| return type(self)(zip(*res), self.positions) | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same order | ||
| and the specified origin :attr:`positions` are equal. | ||
| """ | ||
| try: | ||
| return self is other or ( | ||
| np.array_equal(self.pidx, other.pidx) | ||
| and np.array_equal(self.kz, other.kz) | ||
| and np.array_equal(self.m, other.m) | ||
| and np.array_equal(self.pol, other.pol) | ||
| and np.array_equal(self.positions, other.positions) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| @classmethod | ||
| def default(cls, kzs, mmax, nmax=1, positions=None): | ||
| """Default basis for the given z-components of wave vector and angular momentum. | ||
| The default order contains separate blocks for each position index which are in | ||
| ascending order. Within each block the modes are sorted by the z-component of | ||
| the wave vector :math:`k_z`. For each of those values the z-projection of the | ||
| angular momentum is placed in ascending order. Finally, the polarization index | ||
| is the fastest changing index which iterates between 1 and 0. | ||
| Example: | ||
| >>> CylindricalWaveBasis.default([-0.5, 0.5], 1) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0], | ||
| kz=[-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 0.5 0.5 0.5 0.5 0.5 0.5], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| >>> CylindricalWaveBasis.default([0], 1, 2, [[1., 0, 0], [-1., 0, 0]]) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 1 1 1 1 1 1], | ||
| kz=[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[ 1. 0. 0.], [-1. 0. 0.]], | ||
| ) | ||
| Args: | ||
| kzs (array-like, float): Maximal multipolar order. | ||
| mmax (int): Maximal value of the angular momentum z-component. | ||
| nmax (int, optional): Number of positions, defaults to 1. | ||
| positions (array-like, optional): Positions of the origins. | ||
| """ | ||
| kzs = np.atleast_1d(kzs) | ||
| if kzs.ndim > 1: | ||
| raise ValueError(f"kzs has dimension larger than one: '{kzs.ndim}'") | ||
| modes = [ | ||
| [n, kz, m, p] | ||
| for n in range(nmax) | ||
| for kz in kzs | ||
| for m in range(-mmax, mmax + 1) | ||
| for p in range(1, -1, -1) | ||
| ] | ||
| return cls(modes, positions=positions) | ||
| @classmethod | ||
| def diffr_orders(cls, kz, mmax, lattice, bmax, nmax=1, positions=None): | ||
| """Create a basis set for a system periodic in the z-direction. | ||
| Example: | ||
| >>> CylindricalWaveBasis.diffr_orders(0.1, 1, 2 * np.pi, 1) | ||
| CylindricalWaveBasis( | ||
| pidx=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], | ||
| kz=[-0.9 -0.9 -0.9 -0.9 -0.9 -0.9 0.1 0.1 0.1 0.1 0.1 0.1 1.1 1.1 | ||
| 1.1 1.1 1.1 1.1], | ||
| m=[-1 -1 0 0 1 1 -1 -1 0 0 1 1 -1 -1 0 0 1 1], | ||
| pol=[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0], | ||
| positions=[[0. 0. 0.]], | ||
| ) | ||
| Args: | ||
| kz (float): Wave vector z-component. Ideally it is in the first Brillouin | ||
| zone (use :func:`misc.firstbrillouin1d`). | ||
| mmax (int): Maximal value for the z-component of the angular momentum. | ||
| lattice (:class:`treams.Lattice` or float): Lattice definition or pitch. | ||
| bmax (float): Maximal change of the z-component of the wave vector. So, | ||
| this defines a maximal momentum transfer from the given value `kz`. | ||
| nmax (int, optional): Number of positions. | ||
| positions (array-like, optional): Positions of the origins. | ||
| """ | ||
| lattice = Lattice(lattice) | ||
| lattice_z = Lattice(lattice, "z") | ||
| nkz = np.floor(np.abs(bmax / lattice_z.reciprocal)) | ||
| kzs = kz + np.arange(-nkz, nkz + 1) * lattice_z.reciprocal | ||
| res = cls.default(kzs, mmax, nmax, positions=positions) | ||
| res.lattice = lattice | ||
| res.kpar = WaveVector(kz) | ||
| return res | ||
| @classmethod | ||
| def _from_iterable(cls, it, positions=None): | ||
| if isinstance(cls, CylindricalWaveBasis): | ||
| positions = cls.positions if positions is None else positions | ||
| lattice = cls.lattice | ||
| kpar = cls.kpar | ||
| cls = type(cls) | ||
| else: | ||
| lattice = kpar = None | ||
| obj = cls(it, positions=positions) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| @staticmethod | ||
| def defaultmmax(dim, nkz=1, nmax=1): | ||
| """Calculate the default mode order for a given length. | ||
| Given the dimension of the T-matrix return the estimated maximal value of `m`. | ||
| This is the inverse of :meth:`defaultdim`. A value of zero is allowed for empty | ||
| T-matrices. | ||
| Example: | ||
| >>> CylindricalWaveBasis.defaultmmax(len(CylindricalWaveBasis.default([0], 2)), 1) | ||
| 2 | ||
| Args: | ||
| dim (int): Dimension of the T-matrix, respectively number of modes | ||
| nkz (int, optional): Number of z-components of the wave vector. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| Returns: | ||
| int | ||
| """ | ||
| if dim % (2 * nkz * nmax) != 0: | ||
| raise ValueError("cannot estimate the default mmax") | ||
| dim = dim // (2 * nkz * nmax) | ||
| if (dim - 1) % 2 != 0: | ||
| raise ValueError("cannot estimate the default mmax") | ||
| return (dim - 1) // 2 | ||
| @staticmethod | ||
| def defaultdim(nkz, mmax, nmax=1): | ||
| """Default number of modes for a given mulipolar order. | ||
| Given the maximal value of `l` return the size of the corresponding T-matrix. | ||
| This is the inverse of :meth:`defaultlmax`. A value of zero is allowed. | ||
| Args: | ||
| nkz (int): Number of z-components of the wave vector. | ||
| mmax (int): Maximal value of the angular momentum's z-component. | ||
| nmax (int, optional): Number of particles, defaults to 1. | ||
| Returns: | ||
| int | ||
| """ | ||
| if nkz < 0 or mmax < 0: | ||
| raise ValueError("maximal order must be positive") | ||
| return (2 * mmax + 1) * nkz * 2 * nmax | ||
| class PlaneWaveBasis(BasisSet): | ||
| """Plane wave basis parent class.""" | ||
| isglobal = True | ||
| class PlaneWaveBasisByUnitVector(PlaneWaveBasis): | ||
| """Plane wave basis. | ||
| A plane wave basis is defined by a collection of wave vectors specified by the | ||
| Cartesian wave vector components ``qx``, ``qy``, and ``qz`` normalized to | ||
| :math:`q_x^2 + q_y^2 + q_z^2 = 1` and the polarizations ``pol``. | ||
| For plane waves there exist multiple :ref:`params:Polarizations`, such that | ||
| these modes can refer to either :func:`~treams.special.vpw_A` or | ||
| :func:`~treams.special.vpw_M` and :func:`~treams.special.vpw_N`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``qx``, ``qy``, | ||
| ``qz``, and ``pol``. | ||
| Attributes: | ||
| qx (array-like): X-component of the normalized wave vector. | ||
| qy (array-like): Y-component of the normalized wave vector. | ||
| qz (array-like): Z-component of the normalized wave vector. | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| _names = ("qx", "qy", "qz", "pol") | ||
| """A plane wave basis is always global.""" | ||
| def __init__(self, modes): | ||
| """Initialization.""" | ||
| tmp = [] | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| qx = [] | ||
| qy = [] | ||
| qz = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 4: | ||
| qx, qy, qz, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| qx, qy, qz, pol = (np.array(i) for i in (qx, qy, qz, pol)) | ||
| norm = np.emath.sqrt(qx * qx + qy * qy + qz * qz) | ||
| norm[np.abs(norm - 1) < 1e-14] = 1 | ||
| qx, qy, qz = (np.true_divide(i, norm) for i in (qx, qy, qz)) | ||
| for i in (qx, qy, qz, pol): | ||
| i.flags.writeable = False | ||
| if i.ndim > 1: | ||
| raise ValueError("invalid shape of parameters") | ||
| self.qx, self.qy, self.qz = qx, qy, qz | ||
| self.pol = np.array(pol.real, int) | ||
| if np.any(self.pol != pol): | ||
| raise ValueError("polarizations must be integer") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| self.lattice = self.kpar = None | ||
| def __getattr__(self, key): | ||
| dct = {"x": "qx", "y": "qy", "z": "qz", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| Alternatively, the string "xyzp", "xyp", or "zp" can be used to access only a | ||
| subset of :attr:`qx`, :attr:`qy`, :attr:`qz`, and :attr:`pol`. | ||
| """ | ||
| res = self.qx[idx], self.qy[idx], self.qz[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or (isinstance(idx, tuple) and idx == ()): | ||
| return res | ||
| return type(self)(zip(*res)) | ||
| @classmethod | ||
| def default(cls, kvecs): | ||
| """Default basis from the given wave vectors. | ||
| For each wave vector the two polarizations 1 and 0 are taken. | ||
| Example: | ||
| >>> PlaneWaveBasisByUnitVector.default([[0, 0, 5], [0, 3, 4]]) | ||
| PlaneWaveBasisByUnitVector( | ||
| qx=[0. 0. 0. 0.], | ||
| qy=[0. 0. 0.6 0.6], | ||
| qz=[1. 1. 0.8 0.8], | ||
| pol=[1 0 1 0], | ||
| ) | ||
| Args: | ||
| kvecs (array-like): Wave vectors in Cartesian coordinates. | ||
| """ | ||
| kvecs = np.atleast_2d(kvecs) | ||
| modes = np.empty((2 * kvecs.shape[0], 4), kvecs.dtype) | ||
| modes[::2, :3] = kvecs | ||
| modes[1::2, :3] = kvecs | ||
| modes[::2, 3] = 1 | ||
| modes[1::2, 3] = 0 | ||
| return cls(modes) | ||
| @classmethod | ||
| def _from_iterable(cls, it): | ||
| if isinstance(cls, PlaneWaveBasisByUnitVector): | ||
| lattice = cls.lattice | ||
| kpar = cls.kpar | ||
| cls = type(cls) | ||
| else: | ||
| lattice = kpar = None | ||
| obj = cls(it) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same | ||
| order. | ||
| """ | ||
| try: | ||
| return self is other or ( | ||
| np.array_equal(self.qx, other.qx) | ||
| and np.array_equal(self.qy, other.qy) | ||
| and np.array_equal(self.qz, other.qz) | ||
| and np.array_equal(self.pol, other.pol) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| def bycomp(self, k0, alignment="xy", material=Material()): | ||
| """Create a :class:`PlaneWaveBasisByComp`. | ||
| The plane wave basis is changed to a partial basis, where only two (real-valued) | ||
| wave vector components are defined the third component is then inferred from the | ||
| dispersion relation, which depends on the wave number and material, and a | ||
| ``modetype`` that specifies the sign of the third component. | ||
| Args: | ||
| alignment (str, optional): Wave vector components that are part of the | ||
| partial basis. Defaults to "xy", other permitted values are "yz" and | ||
| "zx". | ||
| k0 (float, optional): Wave number. If given, it is checked that the current | ||
| basis fulfils the dispersion relation. | ||
| material (:class:`~treams.Material` or tuple): Material definition. Defaults | ||
| to vacuum/air. | ||
| """ | ||
| ks = material.ks(k0)[self.pol] | ||
| if alignment in ("xy", "yz", "zx"): | ||
| kpars = [ks * getattr(self, "q" + s) for s in alignment] | ||
| else: | ||
| raise ValueError(f"invalid alignment '{alignment}'") | ||
| obj = PlaneWaveBasisByComp(zip(*kpars, self.pol), alignment) | ||
| obj.lattice = self.lattice | ||
| obj.kpar = self.kpar | ||
| return obj | ||
| def kvecs(self, k0, material=Material(), modetype=None): | ||
| """Wave vectors. | ||
| Args: | ||
| k0 (float): Wave number. | ||
| material (:class:`~treams.Material` or tuple, optional): Material | ||
| definition. Defaults to vacuum/air. | ||
| modetype (optional): Currently unused for this class. | ||
| """ | ||
| # TODO: check kz depending on modetype (alignment?) | ||
| ks = Material(material).ks(k0)[self.pol] | ||
| return ks * self.qx, ks * self.qy, ks * self.qz | ||
| def permute(self, n=1): | ||
| n = n % 3 | ||
| lattice = None if self.lattice is None else self.lattice.permute(n) | ||
| kpar = None if self.kpar is None else self.kpar.permute(n) | ||
| qx, qy, qz = self.qx, self.qy, self.qz | ||
| for _ in range(n): | ||
| qx, qy, qz = qz, qx, qy | ||
| obj = type(self)(zip(*(qx, qy, qz, self.pol))) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| class PlaneWaveBasisByComp(PlaneWaveBasis): | ||
| """Partial plane wave basis. | ||
| A partial plane wave basis is defined by two wave vector components out of all three | ||
| Cartesian wave vector components ``kx``, ``ky``, and ``kz`` and the polarizations | ||
| ``pol``. Which two components are given is specified in the :attr:`alignment`. This | ||
| basis is mostly used for stratified media that are periodic or uniform in the two | ||
| alignment directions, such that the given wave vector components correspond to the | ||
| diffraction orders. | ||
| For plane waves there exist multiple :ref:`params:Polarizations`, such that | ||
| these modes can refer to either :func:`~treams.special.vpw_A` or | ||
| :func:`~treams.special.vpw_M` and :func:`~treams.special.vpw_N`. | ||
| Args: | ||
| modes (array-like): A tuple containing a list for each of ``k1``, ``k2``, and | ||
| ``pol``. | ||
| alignment (str, optional): Definition which wave vector components are given. | ||
| Defaults to "xy", other possible values are "yz" and "zx". | ||
| Attributes: | ||
| alignment (str): Alignment of the partial basis. | ||
| pol (array-like): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| def __init__(self, modes, alignment="xy"): | ||
| """Initialization.""" | ||
| if isinstance(modes, np.ndarray): | ||
| modes = modes.tolist() | ||
| tmp = [] | ||
| for m in modes: | ||
| if m not in tmp: | ||
| tmp.append(m) | ||
| modes = tmp | ||
| if len(modes) == 0: | ||
| kx = [] | ||
| ky = [] | ||
| pol = [] | ||
| elif len(modes[0]) == 3: | ||
| kx, ky, pol = (*zip(*modes),) | ||
| else: | ||
| raise ValueError("invalid shape of modes") | ||
| self._kx, self._ky = [np.real(i) for i in (kx, ky)] | ||
| self.pol = np.array(np.real(pol), int) | ||
| for i, j in [(self._kx, kx), (self._ky, ky), (self.pol, pol)]: | ||
| i.flags.writeable = False | ||
| if i.ndim > 1: | ||
| raise ValueError("invalid shape of parameters") | ||
| if np.any(i != j): | ||
| raise ValueError("invalid value for parameter, must be real") | ||
| if np.any(self.pol > 1) or np.any(self.pol < 0): | ||
| raise ValueError("polarization must be '0' or '1'") | ||
| if alignment in ("xy", "yz", "zx"): | ||
| self._names = (*("k" + i for i in alignment),) + ("pol",) | ||
| else: | ||
| raise ValueError(f"invalid alignment '{alignment}'") | ||
| self.alignment = alignment | ||
| self.lattice = self.kpar = None | ||
| def permute(self, n=1): | ||
| n = n % 3 | ||
| lattice = None if self.lattice is None else self.lattice.permute(n) | ||
| kpar = None if self.kpar is None else self.kpar.permute(n) | ||
| alignments = {"xy": "yz", "yz": "zx", "zx": "xy"} | ||
| alignment = self.alignment | ||
| for _ in range(n): | ||
| alignment = alignments[alignment] | ||
| obj = self._from_iterable(self, alignment=alignment) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| @property | ||
| def kx(self): | ||
| """X-components of the wave vector. | ||
| If the components are not specified `None` is returned. | ||
| """ | ||
| if self.alignment == "xy": | ||
| return self._kx | ||
| if self.alignment == "zx": | ||
| return self._ky | ||
| return None | ||
| @property | ||
| def ky(self): | ||
| """Y-components of the wave vector. | ||
| If the components are not specified `None` is returned. | ||
| """ | ||
| if self.alignment == "xy": | ||
| return self._ky | ||
| if self.alignment == "yz": | ||
| return self._kx | ||
| return None | ||
| @property | ||
| def kz(self): | ||
| """Z-components of the wave vector. | ||
| If the components are not specified `None` is returned. | ||
| """ | ||
| if self.alignment == "yz": | ||
| return self._ky | ||
| if self.alignment == "zx": | ||
| return self._kx | ||
| return None | ||
| def __getattr__(self, key): | ||
| dct = {"x": "kx", "y": "ky", "z": "kz", "s": "pol"} | ||
| try: | ||
| return tuple(getattr(self, dct[k]) for k in key) | ||
| except KeyError: | ||
| raise AttributeError( | ||
| f"'{type(self).__name__}' object has no attribute '{key}'" | ||
| ) from None | ||
| def __getitem__(self, idx): | ||
| """Get a subset of the basis. | ||
| This function allows index into the basis set by an integer, a slice, a sequence | ||
| of integers or bools, an ellipsis, or an empty tuple. All of them except the | ||
| integer and the empty tuple results in another basis set being returned. In case | ||
| of the two exceptions a tuple is returned. | ||
| """ | ||
| res = self._kx[idx], self._ky[idx], self.pol[idx] | ||
| if isinstance(idx, (int, np.integer)) or (isinstance(idx, tuple) and idx == ()): | ||
| return res | ||
| return type(self)(zip(*res)) | ||
| @classmethod | ||
| def default(cls, kpars, alignment="xy"): | ||
| """Default basis from the given wave vectors. | ||
| For each wave vector the two polarizations 1 and 0 are taken. | ||
| Example: | ||
| >>> PlaneWaveBasisByComp.default([[0, 0], [0, 3]]) | ||
| PlaneWaveBasisByComp( | ||
| kx=[0. 0. 0. 0.], | ||
| ky=[0. 0. 3. 3.], | ||
| pol=[1 0 1 0], | ||
| ) | ||
| Args: | ||
| kpars (array-like): Wave vector components in Cartesian coordinates. | ||
| alignment (str, optional): Definition which wave vector components are | ||
| given. Defaults to "xy", other possible values are "yz" and "zx". | ||
| """ | ||
| kpars = np.atleast_2d(kpars) | ||
| modes = np.empty((2 * kpars.shape[0], 3), float) | ||
| modes[::2, :2] = kpars | ||
| modes[1::2, :2] = kpars | ||
| modes[::2, 2] = 1 | ||
| modes[1::2, 2] = 0 | ||
| return cls(modes, alignment=alignment) | ||
| @classmethod | ||
| def diffr_orders(cls, kpar, lattice, bmax): | ||
| """Create a basis set for a two-dimensional periodic system. | ||
| The reciprocal lattice to the given lattice is taken to consider all diffraction | ||
| orders that lie within the defined maximal radius (in reciprocal space). | ||
| Example: | ||
| >>> PlaneWaveBasisByComp.diffr_orders([0, 0], Lattice.square(2 * np.pi), 1) | ||
| PlaneWaveBasisByComp( | ||
| kx=[ 0. 0. 0. 0. 0. 0. 1. 1. -1. -1.], | ||
| ky=[ 0. 0. 1. 1. -1. -1. 0. 0. 0. 0.], | ||
| pol=[1 0 1 0 1 0 1 0 1 0], | ||
| ) | ||
| Args: | ||
| kpar (float): Tangential wave vector components. Ideally they are in the | ||
| first Brillouin zone (use :func:`misc.firstbrillouin2d`). | ||
| lattice (:class:`treams.Lattice` or float): Lattice definition or pitch. | ||
| bmax (float): Maximal change of tangential wave vector components. So, | ||
| this defines a maximal momentum transfer. | ||
| """ | ||
| lattice = Lattice(lattice) | ||
| if lattice.dim != 2: | ||
| raise ValueError("invalid lattice dimensions") | ||
| latrec = lattice.reciprocal | ||
| kpars = kpar + la.diffr_orders_circle(latrec, bmax) @ latrec | ||
| obj = cls.default(kpars, alignment=lattice.alignment) | ||
| obj.lattice = lattice | ||
| obj.kpar = WaveVector(kpar, alignment=lattice.alignment) | ||
| return obj | ||
| @classmethod | ||
| def _from_iterable(cls, it, alignment="xy"): | ||
| if isinstance(cls, PlaneWaveBasisByComp): | ||
| alignment = cls.alignment if alignment is None else alignment | ||
| lattice = cls.lattice | ||
| kpar = cls.kpar | ||
| cls = type(cls) | ||
| else: | ||
| lattice = kpar = None | ||
| obj = cls(it, alignment) | ||
| obj.lattice = lattice | ||
| obj.kpar = kpar | ||
| return obj | ||
| def __eq__(self, other): | ||
| """Compare basis sets. | ||
| Basis sets are considered equal, when they have the same modes in the same | ||
| order. | ||
| """ | ||
| try: | ||
| skx, sky, skz = self.kx, self.ky, self.kz | ||
| okx, oky, okz = other.kx, other.ky, other.kz | ||
| return self is other or ( | ||
| (np.array_equal(skx, okx) or (skx is None and okx is None)) | ||
| and (np.array_equal(sky, oky) or (sky is None and oky is None)) | ||
| and (np.array_equal(skz, okz) or (skz is None and okz is None)) | ||
| and np.array_equal(self.pol, other.pol) | ||
| ) | ||
| except AttributeError: | ||
| return False | ||
| def byunitvector(self, k0, material=Material(), modetype="up"): | ||
| """Create a complete basis :class:`PlaneWaveBasis`. | ||
| A plane wave basis is considered complete, when all three Cartesian components | ||
| and the polarization is defined for each mode. So, the specified wave number, | ||
| material, and modetype is taken to calculate the third Cartesian wave vector. | ||
| The modetype "up" ("down") is for waves propagating in the positive (negative) | ||
| direction with respect to the Cartesian axis that is orthogonal to the | ||
| alignment. | ||
| Args: | ||
| k0 (float): Wave number. | ||
| material (:class:`~treams.Material` or tuple, optional): Material | ||
| definition. Defaults to vacuum/air. | ||
| modetype (str, optional): Propagation direction. Defaults to "up". | ||
| """ | ||
| if modetype not in ("up", "down"): | ||
| raise ValueError("modetype not recognized") | ||
| material = Material(material) | ||
| kx = self._kx | ||
| ky = self._ky | ||
| kz = material.kzs(k0, kx, ky, self.pol) * (2 * (modetype == "up") - 1) | ||
| if self.alignment == "yz": | ||
| kx, ky, kz = kz, kx, ky | ||
| elif self.alignment == "zy": | ||
| kx, ky, kz = ky, kz, kx | ||
| obj = PlaneWaveBasisByUnitVector(zip(kx, ky, kz, self.pol)) | ||
| obj.lattice = self.lattice | ||
| obj.kpar = self.kpar | ||
| return obj | ||
| def kvecs(self, k0, material=Material(), modetype="up"): | ||
| """Wave vectors. | ||
| Args: | ||
| k0 (float): Wave number. | ||
| material (:class:`~treams.Material` or tuple, optional): Material | ||
| definition. Defaults to vacuum/air. | ||
| modetype (str, optional): Propagation direction. Defaults to "up". | ||
| """ | ||
| if modetype not in ("up", "down"): | ||
| raise ValueError("modetype not recognized") | ||
| material = Material(material) | ||
| kx = self._kx | ||
| ky = self._ky | ||
| kz = material.kzs(k0, kx, ky, self.pol) * (2 * (modetype == "up") - 1) | ||
| if self.alignment == "yz": | ||
| return kz, kx, ky | ||
| if self.alignment == "zx": | ||
| return ky, kz, kx | ||
| return kx, ky, kz | ||
| def _raise_basis_error(*args): | ||
| raise TypeError("'basis' must be BasisSet") | ||
| class PhysicsDict(util.AnnotationDict): | ||
| """Physics dictionary. | ||
| Derives from :class:`treams.util.AnnotationDict`. This dictionary has additionally | ||
| several properties defined. | ||
| Attributes: | ||
| basis (:class:`BasisSet`): Basis set. | ||
| k0 (float): Wave number. | ||
| kpar (list): Parallel wave vector components. Usually, this is a list of length | ||
| 3 with its items corresponding to the Cartesian axes. Unspecified items are | ||
| set to `nan`. | ||
| lattice (:class:`~treams.Lattice`): Lattice definition. | ||
| material (:class:`~treams.Material`): Material definition. | ||
| modetype (str): Mode type, for spherical and cylindrical waves this can be | ||
| "incident" and "scattered", for partial plane waves it can be "up" or | ||
| "down". | ||
| poltype (str): Polarization, see also :ref:`params:Polarizations`. | ||
| """ | ||
| properties = { | ||
| "basis": ( | ||
| ":class:`~treams.BasisSet`.", | ||
| lambda x: isinstance(x, BasisSet), | ||
| _raise_basis_error, | ||
| ), | ||
| "k0": ("Wave number.", lambda x: isinstance(x, float), float), | ||
| "kpar": ( | ||
| "Wave vector components tangential to the lattice.", | ||
| lambda x: isinstance(x, WaveVector), | ||
| WaveVector, | ||
| ), | ||
| "lattice": ( | ||
| ":class:`~treams.Lattice`.", | ||
| lambda x: isinstance(x, Lattice), | ||
| Lattice, | ||
| ), | ||
| "material": ( | ||
| ":class:`~treams.Material`.", | ||
| lambda x: isinstance(x, Material), | ||
| Material, | ||
| ), | ||
| "modetype": ("Mode type.", lambda x: isinstance(x, str), str), | ||
| "poltype": (":ref:`params:Polarizations`.", lambda x: isinstance(x, str), str,), | ||
| } | ||
| """Special properties tracked by the PhysicsDict.""" | ||
| def __setitem__(self, key, val): | ||
| """Set item specified by key to the defined value. | ||
| When overwriting an existing key an :class:`AnnotationWarning` is emitted. | ||
| Avoid the warning by explicitly deleting the key first. The special attributes | ||
| are cast to their corresponding types. | ||
| Args: | ||
| key (hashable): Key | ||
| val : Value | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| if key not in self.properties: | ||
| raise AttributeError(f"invalid key '{key}'") | ||
| _, testfunc, castfunc = self.properties[key] | ||
| if not testfunc(val): | ||
| val = castfunc(val) | ||
| super().__setitem__(key, val) | ||
| class PhysicsArray(util.AnnotatedArray): | ||
| """Physics-aware array. | ||
| A physics aware array is a special type of :class`~treams.util.AnnotatedArray`. | ||
| Additionally to keeping track of the annotations, it is enhanced by the ability to | ||
| create suiting linear operators to perform tasks like rotations, translations, or | ||
| expansions into different basis sets and by applying special rules for the | ||
| physical properties of :class:`PhysicsDict` upon matrix multiplications, see also | ||
| :meth:`__matmul__`. | ||
| """ | ||
| _scales = {"basis"} | ||
| changepoltype = op.OperatorAttribute(op.ChangePoltype) | ||
| """Polarization change matrix, see also :class:`treams.operators.ChangePoltype`.""" | ||
| efield = op.OperatorAttribute(op.EField) | ||
| hfield = op.OperatorAttribute(op.HField) | ||
| dfield = op.OperatorAttribute(op.DField) | ||
| bfield = op.OperatorAttribute(op.BField) | ||
| gfield = op.OperatorAttribute(op.GField) | ||
| ffield = op.OperatorAttribute(op.FField) | ||
| """Field evaluation matrix, see also :class:`treams.operators.EField`.""" | ||
| expand = op.OperatorAttribute(op.Expand) | ||
| """Expansion matrix, see also :class:`treams.operators.Expand`.""" | ||
| expandlattice = op.OperatorAttribute(op.ExpandLattice) | ||
| """Lattice expansion matrix, see also :class:`treams.operators.ExpandLattice`.""" | ||
| permute = op.OperatorAttribute(op.Permute) | ||
| """Permutation matrix, see also :class:`treams.operators.Permute`.""" | ||
| rotate = op.OperatorAttribute(op.Rotate) | ||
| """Rotation matrix, see also :class:`treams.operators.Rotate`.""" | ||
| translate = op.OperatorAttribute(op.Translate) | ||
| """Translation matrix, see also :class:`treams.operators.Translate`.""" | ||
| def __init__(self, arr, ann=(), /, **kwargs): | ||
| """Initialization.""" | ||
| super().__init__(arr, ann, **kwargs) | ||
| self._check() | ||
| @property | ||
| def ann(self): | ||
| """Array annotations.""" | ||
| # necessary to define the setter below | ||
| return super().ann | ||
| def __getattr__(self, key): | ||
| try: | ||
| return super().__getattr__(key) | ||
| except AttributeError as err: | ||
| if key in PhysicsDict.properties: | ||
| return None | ||
| raise err from None | ||
| def __setattr__(self, key, val): | ||
| if key in PhysicsDict.properties: | ||
| val = (val,) * self.ndim if not isinstance(val, tuple) else val | ||
| self.ann.as_dict[key] = val | ||
| else: | ||
| super().__setattr__(key, val) | ||
| @ann.setter | ||
| def ann(self, ann): | ||
| """Set array annotations. | ||
| See also :meth:`treams.util.AnnotatedArray.__setitem__`. | ||
| """ | ||
| self._ann = util.AnnotationSequence(*(({},) * self.ndim), mapping=PhysicsDict) | ||
| self._ann.update(ann) | ||
| def __repr__(self): | ||
| """String representation. | ||
| For a more managable output only the special physics properties are shown | ||
| alongside the array itself. | ||
| """ | ||
| repr_arr = " " + repr(self._array)[6:-1].replace("\n ", "\n") + "," | ||
| for key in PhysicsDict.properties: | ||
| if getattr(self, key) is not None: | ||
| repr_arr += f"\n {key}={repr(getattr(self, key))}," | ||
| return f"{self.__class__.__name__}(\n{repr_arr}\n)" | ||
| def _check(self): | ||
| """Run checks to validate the physical properties. | ||
| The checks are run on the last two dimensions. They include: | ||
| * Dispersion relation checks for :class:`PlaneWaveBasis` if `basis`, `k0` | ||
| and `material` is defined` | ||
| * `parity` polarizations are not physically permitted in chiral media. | ||
| * All lattices explicitly given and in the basis hints must be compatible. | ||
| * All tangential wave vector compontents must be compatible. | ||
| """ | ||
| for a in self.ann[-2:]: | ||
| k0 = a.get("k0") | ||
| material = a.get("material") | ||
| modetype = a.get("modetype") | ||
| poltype = a.get("poltype") | ||
| basis = a.get("basis", namedtuple("_basis", "lattice kpar")(None, None)) | ||
| if poltype == "parity" and getattr(material, "ischiral", False): | ||
| raise ValueError("poltype 'parity' not permitted for chiral material") | ||
| def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): | ||
| """Implement ufunc API. | ||
| Additionally to keeping track of the annotations the special properties of a | ||
| PhysicsArray are also "transparent" in matrix multiplications. | ||
| """ | ||
| res = super().__array_ufunc__(ufunc, method, *inputs, **kwargs) | ||
| if ( | ||
| ufunc is np.matmul | ||
| and method == "__call__" | ||
| and not isinstance(res, np.generic) | ||
| ): | ||
| axes = kwargs.get( | ||
| "axes", [tuple(range(-min(np.ndim(i), 2), 0)) for i in inputs + (res,)] | ||
| ) | ||
| anns = [ | ||
| i.ann[ax] if hasattr(i, "ann") else [{}, {}] | ||
| for i, ax in zip(inputs, axes) | ||
| ] | ||
| for name in PhysicsDict.properties: | ||
| if name in anns[0][-1] and all(name not in a for a in anns[1]): | ||
| res.ann[axes[-1][-1]].setdefault(name, anns[0][-1][name]) | ||
| if name in anns[1][0] and all(name not in a for a in anns[0]): | ||
| res.ann[axes[-1][0]].setdefault(name, anns[1][0][name]) | ||
| return res |
| import collections | ||
| import numpy as np | ||
| import treams.lattice as la | ||
| class Lattice: | ||
| """Lattice definition. | ||
| The lattice can be one-, two-, or three-dimensional. If it is not three-dimensional | ||
| it is required to be embedded into a lower dimensional subspace that is aligned with | ||
| the Cartesian axes. The default alignment for one-dimensional lattices is along the | ||
| z-axis and for two-dimensional lattices it is in the x-y-plane. | ||
| For a one-dimensional lattice the definition consists of simply the value of the | ||
| lattice pitch. Higher-dimensional lattices are defined by (2, 2)- or (3, 3)-arrays. | ||
| If the arrays are diagonal it is sufficient to specify the (2,)- or (3,)-array | ||
| diagonals. Alternatively, if another instance of a Lattice is given, the defined | ||
| alignment is extracted, which can be used to separate a lower-dimensional | ||
| sublattice. | ||
| Lattices are immutable objects. | ||
| Example: | ||
| >>> Lattice([1, 2]) | ||
| Lattice([[1. 0.] | ||
| [0. 2.]], alignment='xy') | ||
| >>> Lattice(_, 'x') | ||
| Lattice(1.0, alignment='x') | ||
| Args: | ||
| arr (float, array, Lattice): Lattice definition. Each row corresponds to one | ||
| lattice vector, each column to the axis defined in `alignment`. | ||
| alignment (str, optional): Alignment of the lattice. Possible values are 'x', | ||
| 'y', 'z', 'xy', 'yz', 'zx', and 'xyz'. | ||
| """ | ||
| _allowed_alignments = { | ||
| "x": "y", | ||
| "y": "z", | ||
| "z": "x", | ||
| "xy": "yz", | ||
| "yz": "zx", | ||
| "zx": "xy", | ||
| "xyz": "xyz", | ||
| } | ||
| def __init__(self, arr, alignment=None): | ||
| """Initialization.""" | ||
| if isinstance(arr, Lattice): | ||
| self._alignment = arr.alignment if alignment is None else alignment | ||
| self._lattice = arr._sublattice(self._alignment)[...] | ||
| self._reciprocal = ( | ||
| 2 * np.pi / self[...] if self.dim == 1 else la.reciprocal(self[...]) | ||
| ) | ||
| return | ||
| if arr is None: | ||
| raise ValueError("Lattice cannot be 'None'") | ||
| arr = np.array(arr, float) | ||
| arr.flags.writeable = False | ||
| alignments = {1: ("z", "x", "y"), 4: ("xy", "yz", "zx"), 9: ("xyz",)} | ||
| if arr.ndim < 3 and arr.size == 1: | ||
| self._lattice = np.squeeze(arr) | ||
| elif arr.ndim == 1 and arr.size in (2, 3): | ||
| self._lattice = np.diag(arr) | ||
| elif arr.ndim == 2 and arr.shape[0] == arr.shape[1] and arr.shape[0] in (2, 3): | ||
| self._lattice = arr | ||
| else: | ||
| raise ValueError(f"invalid shape '{arr.shape}'") | ||
| alignment = ( | ||
| alignments[self._lattice.size][0] if alignment is None else alignment | ||
| ) | ||
| if alignment not in alignments[self._lattice.size]: | ||
| raise ValueError(f"invalid alignment '{alignment}'") | ||
| self._alignment = alignment | ||
| if self.volume == 0: | ||
| raise ValueError("linearly dependent lattice vectors") | ||
| self._reciprocal = ( | ||
| 2 * np.pi / self[...] if self.dim == 1 else la.reciprocal(self[...]) | ||
| ) | ||
| @property | ||
| def alignment(self): | ||
| """The alignment of the lattice in three-dimensional space. | ||
| For three-dimensional lattices it is always 'xyz' but lower-dimensional lattices | ||
| have to be aligned with a subset of the axes. | ||
| Returns: | ||
| str | ||
| """ | ||
| return self._alignment | ||
| @classmethod | ||
| def square(cls, pitch, alignment=None): | ||
| """Create a two-dimensional square lattice. | ||
| Args: | ||
| pitch (float): Lattice constant. | ||
| alignment (str, optional): Alignment of the two-dimensional lattice in the | ||
| three-dimensional space. Defaults to 'xy'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls(2 * [pitch], alignment) | ||
| @classmethod | ||
| def cubic(cls, pitch, alignment=None): | ||
| """Create a three-dimensional cubic lattice. | ||
| Args: | ||
| pitch (float): Lattice constant. | ||
| alignment (str, optional): Alignment of the lattice. Defaults to 'xyz'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls(3 * [pitch], alignment) | ||
| @classmethod | ||
| def rectangular(cls, x, y, alignment=None): | ||
| """Create a two-dimensional rectangular lattice. | ||
| Args: | ||
| x (float): Lattice constant along the first dimension. For the default | ||
| alignment this corresponds to the x-axis. | ||
| y (float): Lattice constant along the second dimension. For the default | ||
| alignment this corresponds to the y-axis. | ||
| alignment (str, optional): Alignment of the two-dimensional lattice in the | ||
| three-dimensional space. Defaults to 'xy'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls([x, y], alignment) | ||
| @classmethod | ||
| def orthorhombic(cls, x, y, z, alignment=None): | ||
| """Create a three-dimensional orthorhombic lattice. | ||
| Args: | ||
| x (float): Lattice constant along the first dimension. For the default | ||
| alignment this corresponds to the x-axis. | ||
| y (float): Lattice constant along the second dimension. For the default | ||
| alignment this corresponds to the y-axis. | ||
| z (float): Lattice constant along the third dimension. For the default | ||
| alignment this corresponds to the z-axis. | ||
| alignment (str, optional): Alignment of the lattice. Defaults to 'xyz'. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return cls([x, y, z], alignment) | ||
| @classmethod | ||
| def hexagonal(cls, pitch, height=None, alignment=None): | ||
| """Create a hexagonal lattice. | ||
| The lattice is two-dimensional if no height is specified else it is | ||
| three-dimensional | ||
| Args: | ||
| pitch (float): Lattice constant. | ||
| height (float, optional): Separation along the third axis for a | ||
| three-dimensional lattice. | ||
| alignment (str, optional): Alignment of the two-dimensional lattice in the | ||
| three-dimensional space. Defaults to either 'xy' or 'xyz' in the two- | ||
| or three-dimensional case, respectively. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if height is None: | ||
| return cls( | ||
| np.array([[pitch, 0], [0.5 * pitch, np.sqrt(0.75) * pitch]]), alignment | ||
| ) | ||
| return cls( | ||
| np.array( | ||
| [[pitch, 0, 0], [0.5 * pitch, np.sqrt(0.75) * pitch, 0], [0, 0, height]] | ||
| ), | ||
| alignment, | ||
| ) | ||
| def __eq__(self, other): | ||
| """Equality. | ||
| Two lattices are considered equal when they have the same dimension, alignment, | ||
| and lattice vectors. | ||
| Args: | ||
| other (Lattice): Lattice to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return other is not None and ( | ||
| self is other | ||
| or ( | ||
| self.alignment == other.alignment | ||
| and self.dim == other.dim | ||
| and np.all(self[...] == other[...]) | ||
| ) | ||
| ) | ||
| @property | ||
| def dim(self): | ||
| """Dimension of the lattice. | ||
| Returns: | ||
| int | ||
| """ | ||
| if self._lattice.ndim == 0: | ||
| return 1 | ||
| return self._lattice.shape[0] | ||
| @property | ||
| def volume(self): | ||
| """(Generalized) volume of the lattice. | ||
| The value gives the lattice pitch, area, or volume depending on its dimension. | ||
| The volume is signed. | ||
| Returns: | ||
| float | ||
| """ | ||
| if self.dim == 1: | ||
| return self._lattice | ||
| return la.volume(self._lattice) | ||
| @property | ||
| def reciprocal(self): | ||
| r"""Reciprocal lattice. | ||
| The reciprocal lattice to a given lattice with dimension :math:`d` and lattice | ||
| vectors :math:`\boldsymbol a_i` for :math:`i \in \{1, \dots, d\}` is defined by | ||
| lattice vectors :math:`\boldsymbol b_j` with :math:`j \in \{1, \dots, d\}` such | ||
| that :math:`\boldsymbol a_i \boldsymbol b_j = 2 \pi \delta_{ij}` is fulfilled. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| return self._reciprocal | ||
| def __getitem__(self, idx): | ||
| """Index into the lattice. | ||
| Indexing can be used to obtain entries from the lattice vector definitions or | ||
| by using the Ellipsis or empty tuple to obtain the full array. | ||
| Returns: | ||
| float | ||
| """ | ||
| return self._lattice[idx] | ||
| def __str__(self): | ||
| """String representation. | ||
| Simply returns the lattice pitch or lattice vectors. | ||
| Returns: | ||
| str | ||
| """ | ||
| return str(self._lattice) | ||
| def __repr__(self): | ||
| """Representation. | ||
| The result can be used to recreate an equal instance. | ||
| Returns: | ||
| str | ||
| """ | ||
| string = str(self._lattice).replace("\n", "\n ") | ||
| return f"Lattice({string}, alignment='{self.alignment}')" | ||
| def _sublattice(self, key): | ||
| """Get the sublattice defined by key. | ||
| This function is called when one gives another instance of Lattice to the | ||
| constructor. | ||
| Args: | ||
| key (str): Alignment of the sublattice to extract. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| key = key.lower() | ||
| if self.dim == 1: | ||
| if key == self.alignment: | ||
| return Lattice(self[...], key) | ||
| raise ValueError(f"sublattice with key '{key}' not availale") | ||
| idx = [] | ||
| for c in key: | ||
| idx.append(self.alignment.find(c)) | ||
| if -1 in idx or key not in self._allowed_alignments: | ||
| raise ValueError(f"sublattice with key '{key}' not availale") | ||
| idx_opp = [i for i in range(self.dim) if i not in idx] | ||
| mask = np.any(self[:, idx] != 0, axis=-1) | ||
| if ( | ||
| (self[mask, :][:, idx_opp] != 0).any() | ||
| or (self[np.logical_not(mask), :][:, idx] != 0).any() | ||
| or len(idx) != sum(mask) | ||
| ): | ||
| raise ValueError("cannot determine sublattice") | ||
| return Lattice(self[mask, :][:, idx], key) | ||
| def permute(self, n=1): | ||
| """Permute the lattice orientation. | ||
| Get a new lattice with the alignment permuted. | ||
| Examples: | ||
| >>> Lattice.hexagonal(1, 2).permute() | ||
| Lattice([[0. 1. 0. ] | ||
| [0. 0.5 0.866] | ||
| [2. 0. 0. ]], alignment='xyz') | ||
| >>> Lattice.hexagonal(1).permute() | ||
| Lattice([[1. 0. ] | ||
| [0.5 0.866]], alignment='yz') | ||
| Args: | ||
| n (int, optional): Number of repeated permutations. Defaults to `1`. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if n != int(n): | ||
| raise ValueError("'n' must be integer") | ||
| n = n % 3 | ||
| lattice = self[...] | ||
| alignment = self.alignment | ||
| if self.dim == 3: | ||
| while n > 0: | ||
| lattice = lattice[:, [2, 0, 1]] | ||
| n -= 1 | ||
| else: | ||
| while n > 0: | ||
| alignment = self._allowed_alignments[alignment] | ||
| n -= 1 | ||
| return Lattice(lattice, alignment) | ||
| def __bool__(self): | ||
| """Lattice instances always equate to True. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return True | ||
| def __or__(self, other): | ||
| """Merge two lattices. | ||
| Two lattices are combined into one if possible. | ||
| Example: | ||
| >>> Lattice(1, 'x') | Lattice(2) | ||
| Lattice([[2. 0.] | ||
| [0. 1.]], alignment='zx') | ||
| Args: | ||
| other (Lattice): Lattice to merge. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if other is None or self == other: | ||
| return Lattice(self) | ||
| alignment = list({c for lat in (self, other) for c in lat.alignment}) | ||
| alignment = "".join(sorted(alignment)) | ||
| if alignment == "xz": | ||
| alignment = "zx" | ||
| ndim = len(alignment) | ||
| if ndim == 2: | ||
| if self.dim == 1 == other.dim: | ||
| if alignment.find(self.alignment) == 0: | ||
| return Lattice([self[...], other[...]], alignment) | ||
| return Lattice([other[...], self[...]], alignment) | ||
| if self.dim == 2 and Lattice(self, other.alignment) == other: | ||
| return Lattice(self) | ||
| if other.dim == 2 and Lattice(other, self.alignment) == self: | ||
| return Lattice(other) | ||
| elif ndim == 3: | ||
| if self.dim == 3 and Lattice(self, other.alignment) == other: | ||
| return Lattice(self) | ||
| if other.dim == 3 and Lattice(other, self.alignment) == self: | ||
| return Lattice(other) | ||
| if self.dim == 2 == other.dim: | ||
| arr = np.zeros(3) | ||
| for i, c in enumerate("xyz"): | ||
| if c in self.alignment: | ||
| la0 = Lattice(self, c) | ||
| else: | ||
| la0 = None | ||
| if c in other.alignment: | ||
| la1 = Lattice(other, c) | ||
| else: | ||
| la1 = None | ||
| if None not in (la0, la1) and la0 != la1: | ||
| raise ValueError("cannot combine lattices") | ||
| arr[i] = la0[...] if la0 is not None else la1[...] | ||
| return Lattice(arr) | ||
| arr = np.zeros((3, 3)) | ||
| la0, la1 = (self, other) if self.dim == 1 else (other, self) | ||
| if la0.alignment == "x": | ||
| arr[0, 0] = la0[...] | ||
| arr[1:, 1:] = la1[...] | ||
| return Lattice(arr) | ||
| if la0.alignment == "y": | ||
| arr[1, 1] = la0[...] | ||
| arr[[[2], [0]], [2, 0]] = la1[...] | ||
| return Lattice(arr) | ||
| if la0.alignment == "z": | ||
| arr[2, 2] = la0[...] | ||
| arr[:2, :2] = la1[...] | ||
| return Lattice(arr) | ||
| raise ValueError("cannot combine lattices") | ||
| def __and__(self, other): | ||
| """Intersect two lattices. | ||
| The intersection of two lattices is taken if possible. | ||
| Example: | ||
| >>> Lattice([1, 2]) & Lattice([2, 3], 'yz') | ||
| Lattice(2.0, alignment='y') | ||
| Args: | ||
| other (Lattice): Lattice to intersect. | ||
| Returns: | ||
| Lattice | ||
| """ | ||
| if other is None: | ||
| return None | ||
| if self == other: | ||
| return Lattice(self) | ||
| alignment = list({c for c in self.alignment if c in other.alignment}) | ||
| if len(alignment) == 0: | ||
| raise ValueError("cannot combine lattices") | ||
| alignment = "".join(sorted(alignment)) | ||
| if alignment == "xz": | ||
| alignment = "zx" | ||
| a, b = Lattice(self, alignment), Lattice(other, alignment) | ||
| if a == b: | ||
| return a | ||
| raise ValueError("cannot combine lattices") | ||
| def __le__(self, other): | ||
| """Test if one lattice includes another. | ||
| Example: | ||
| >>> Lattice(3) <= Lattice([1, 2, 3]) | ||
| True | ||
| Args: | ||
| other (Lattice): Lattice to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| try: | ||
| lat = Lattice(other, self.alignment) | ||
| except ValueError: | ||
| return False | ||
| return lat == self | ||
| def isdisjoint(self, other): | ||
| """Test if lattices are disjoint. | ||
| Lattices are considered disjoint if their alignments are disjoint. | ||
| Example: | ||
| >>> Lattice([1, 2, 3]).isdisjoint(Lattice(1)) | ||
| False | ||
| Args: | ||
| other (Lattice): Lattice to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| for c in other.alignment: | ||
| if c in self.alignment: | ||
| return False | ||
| return True | ||
| class WaveVector(collections.abc.Sequence): | ||
| def __init__(self, seq=(), alignment=None): | ||
| try: | ||
| length = len(seq) | ||
| except TypeError: | ||
| length = 1 | ||
| seq = [seq] | ||
| if length == 3: | ||
| self._vec = tuple(seq) | ||
| elif length == 2: | ||
| if alignment in ("xy", None): | ||
| self._vec = (seq[0], seq[1], np.nan) | ||
| elif alignment == "yz": | ||
| self._vec = (np.nan, seq[0], seq[1]) | ||
| elif alignment == "zx": | ||
| self._vec = (seq[1], np.nan, seq[0]) | ||
| else: | ||
| raise ValueError(f"invalid alignment: {alignment}") | ||
| elif length == 1: | ||
| if alignment in ("z", None): | ||
| self._vec = (np.nan, np.nan, seq[0]) | ||
| elif alignment == "y": | ||
| self._vec = (np.nan, seq[0], np.nan) | ||
| elif alignment == "x": | ||
| self._vec = (seq[0], np.nan, np.nan) | ||
| else: | ||
| raise ValueError(f"invalid alignment: {alignment}") | ||
| elif length == 0: | ||
| self._vec = (np.nan,) * 3 | ||
| else: | ||
| raise ValueError("invalid sequence") | ||
| def __str__(self): | ||
| return str(self._vec) | ||
| def __repr__(self): | ||
| return self.__class__.__name__ + str(self._vec) | ||
| def __eq__(self, other): | ||
| other = WaveVector(other) | ||
| for a, b in zip(self, other): | ||
| if a != b and not (np.isnan(a) and np.isnan(b)): | ||
| return False | ||
| return True | ||
| def __len__(self): | ||
| return 3 | ||
| def __getitem__(self, key): | ||
| return self._vec[key] | ||
| def __or__(self, other): | ||
| other = WaveVector(other) | ||
| seq = () | ||
| for a, b in zip(self, other): | ||
| isnan = np.isnan(a) or np.isnan(b) | ||
| if a != b and not isnan: | ||
| raise ValueError("non-matching WaveVector") | ||
| seq += (np.nan,) if isnan else (a,) | ||
| return WaveVector(seq) | ||
| def __and__(self, other): | ||
| other = WaveVector(other) | ||
| seq = () | ||
| for a, b in zip(self, other): | ||
| if a != b and not (np.isnan(a) or np.isnan(b)): | ||
| raise ValueError("non-matching WaveVector") | ||
| seq += (b,) if np.isnan(a) else (a,) | ||
| return WaveVector(seq) | ||
| def permute(self, n=1): | ||
| x, y, z = self | ||
| n = n % 3 | ||
| for _ in range(n): | ||
| x, y, z = z, x, y | ||
| return WaveVector((x, y, z)) | ||
| def __le__(self, other): | ||
| other = WaveVector(other) | ||
| for a, b in zip(self, other): | ||
| if a != b and not np.isnan(b): | ||
| return False | ||
| return True | ||
| def isdisjoint(self, other): | ||
| other = WaveVector(other) | ||
| for a, b in zip(self, other): | ||
| if not (np.isnan(a) or np.isnan(b)): | ||
| return False | ||
| return True |
| import numpy as np | ||
| from treams import misc | ||
| class Material: | ||
| r"""Material definition. | ||
| The material properties are defined in the frequency domain through scalar values | ||
| for permittivity :math:`\epsilon`, permeability :math:`\mu`, and the chirality | ||
| parameter :math:`\kappa`. Materials are, thus, assumed to be linear, | ||
| time-invariant, homogeneous, isotropic, and local. Also, it is assumed that they | ||
| have no gain. The relation of the electric and magnetic fields is defined by | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| \frac{1}{\epsilon_0} \boldsymbol D \\ | ||
| c \boldsymbol B | ||
| \end{pmatrix} | ||
| = | ||
| \begin{pmatrix} | ||
| \epsilon & \mathrm i \kappa \\ | ||
| -\mathrm i \kappa & \mu | ||
| \end{pmatrix} | ||
| \begin{pmatrix} | ||
| \boldsymbol E \\ | ||
| Z_0 \boldsymbol H | ||
| \end{pmatrix} | ||
| for a fixed vacuum wave number :math:`k_0` and spatial position | ||
| :math:`\boldsymbol r` with :math:`\epsilon_0` the vacuum permittivity, :math:`c` the | ||
| speed of light in vacuum, and :math:`Z_0` the vacuum impedance. | ||
| Args: | ||
| epsilon (optional, complex): Relative permittivity. Defaults to 1. | ||
| mu (optional, complex): Relative permeability. Defaults to 1. | ||
| kappa (optional, complex): Chirality parameter. Defaults to 0. | ||
| """ | ||
| def __init__(self, epsilon=1, mu=1, kappa=0): | ||
| """Initialization.""" | ||
| if isinstance(epsilon, Material): | ||
| epsilon, mu, kappa = epsilon() | ||
| elif isinstance(epsilon, (tuple, list, np.ndarray)): | ||
| if len(epsilon) == 0: | ||
| epsilon = 1 | ||
| elif len(epsilon) == 1: | ||
| epsilon = epsilon[0] | ||
| elif len(epsilon) == 2: | ||
| epsilon, mu = epsilon | ||
| elif len(epsilon) == 3: | ||
| epsilon, mu, kappa = epsilon | ||
| else: | ||
| raise ValueError("invalid material definition") | ||
| self._epsilon = epsilon | ||
| self._mu = mu | ||
| self._kappa = kappa | ||
| @property | ||
| def epsilon(self): | ||
| """Relative permittivity. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| return self._epsilon | ||
| @property | ||
| def mu(self): | ||
| """Relative permeability. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| return self._mu | ||
| @property | ||
| def kappa(self): | ||
| """Chirality parameter. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| return self._kappa | ||
| def __iter__(self): | ||
| """Iterator for a tuple containing the material parameters. | ||
| Useful for unpacking the material parameters into a function that takes these | ||
| parameters separately, e.g. ``foo(*material)``. | ||
| """ | ||
| return iter((self.epsilon, self.mu, self.kappa)) | ||
| @classmethod | ||
| def from_n(cls, n=1, impedance=None, kappa=0): | ||
| r"""Create material from refractive index and relative impedance. | ||
| This function calculates the relative permeability and permittivity with | ||
| :math:`\epsilon = \frac{n}{Z}` and :math:`\mu = nZ`. The chirality parameter is | ||
| considered separately. | ||
| Note: | ||
| The refractive index is defined independently from the chirality parameter | ||
| here. For an alternative definition of the refractive index, see also | ||
| :func:`Material.from_nmp`. | ||
| Args: | ||
| n (complex, optional): Refractive index. Defaults to 1. | ||
| impedance(complex, optional): Relative impedance. Defaults to the inverse | ||
| of the refractive index. | ||
| kappa (complex, optional): Chirality parameter. Defaults to 0. | ||
| Returns: | ||
| Material | ||
| """ | ||
| impedance = 1 / n if impedance is None else impedance | ||
| epsilon = n / impedance | ||
| mu = n * impedance | ||
| return cls(epsilon, mu, kappa) | ||
| @classmethod | ||
| def from_nmp(cls, ns=(1, 1), impedance=None): | ||
| r"""Create material from refractive indices of both helicities. | ||
| This function calculates the relative permeability and permittivity and the | ||
| chirality parameter with :math:`\epsilon = \frac{n_+ + n_-}{2Z}`, | ||
| :math:`\mu = \frac{(n_+ + n_-)Z}{2}` and :math:`\mu = \frac{(n_+ - n_-)}{2}`. | ||
| Note: | ||
| Two refractive indices are defined here that depend on the chirality | ||
| parameter. For an alternative definition of the refractive index, see also | ||
| :func:`Material.from_n`. | ||
| Args: | ||
| ns ((2,)-array-like, optional): Negative and positive helicity refractive | ||
| index. Defaults to (1, 1). | ||
| impedance(complex, optional): Relative impedance. Defaults to the inverse of | ||
| the refractive index. | ||
| Returns: | ||
| Material | ||
| """ | ||
| n = sum(ns) * 0.5 | ||
| kappa = (ns[1] - ns[0]) * 0.5 | ||
| return cls.from_n(n, impedance, kappa) | ||
| @property | ||
| def n(self): | ||
| r"""Refractive index. | ||
| The refractive index is defined by :math:`n = \sqrt{\epsilon \mu}`, with an | ||
| enforced non-negative imaginary part. | ||
| Note: | ||
| The refractive index returned is independent from the chirality parameter. | ||
| For an alternative definition of the refractive index, see also | ||
| :func:`Material.nmp`. | ||
| Returns: | ||
| complex | ||
| """ | ||
| n = np.sqrt(self.epsilon * self.mu) | ||
| if n.imag < 0: | ||
| n = -n | ||
| return n | ||
| @property | ||
| def nmp(self): | ||
| r"""Refractive indices of both helicities. | ||
| The refractive indices are defined by | ||
| :math:`n_\pm = \sqrt{\epsilon \mu} \pm \kappa`, with an enforced non-negative | ||
| imaginary part. | ||
| Note: | ||
| The refractive indices returned depend on the chirality parameter. For an | ||
| alternative definition of the refractive index, see also :func:`Material.n`. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| return misc.refractive_index(self.epsilon, self.mu, self.kappa) | ||
| @property | ||
| def impedance(self): | ||
| r"""Relative impedance. | ||
| The relative impedance is defined by :math:`Z = \sqrt{\frac{\epsilon}{\mu}}`. | ||
| Returns: | ||
| complex | ||
| """ | ||
| return np.sqrt(self.mu / self.epsilon) | ||
| def __call__(self): | ||
| """Return a tuple containing all material parameters. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| return self.epsilon, self.mu, self.kappa | ||
| def __eq__(self, other): | ||
| """Compare material parameters. | ||
| Materials are considered equal, when all material parameters are equal. Also, | ||
| compares with objects that contain at most three values. | ||
| Returns: | ||
| bool | ||
| """ | ||
| if other is None: | ||
| return False | ||
| if not isinstance(other, Material): | ||
| other = Material(*other) | ||
| return ( | ||
| self.epsilon == other.epsilon | ||
| and self.mu == other.mu | ||
| and self.kappa == other.kappa | ||
| ) | ||
| @property | ||
| def ischiral(self): | ||
| """Test if the material is chiral. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return self.kappa != 0 | ||
| @property | ||
| def isreal(self): | ||
| """Test if the material has purely real parameters. | ||
| Returns: | ||
| bool | ||
| """ | ||
| return all(i.imag == 0 for i in self) | ||
| def __str__(self): | ||
| """All three material parameters. | ||
| Returns: | ||
| str | ||
| """ | ||
| return "(" + ", ".join([str(i) for i in self()]) + ")" | ||
| def __repr__(self): | ||
| """Representation that allows recreating the object. | ||
| Returns: | ||
| str | ||
| """ | ||
| return self.__class__.__name__ + str(self) | ||
| def ks(self, k0): | ||
| """Return the wave numbers in the medium for both polarizations. | ||
| The first value corresponds to negative helicity and the second to positive | ||
| helicity. For achiral materials where parity polarizations can be used both | ||
| values are equal. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| return k0 * self.nmp | ||
| def krhos(self, k0, kz, pol): | ||
| r"""The (cylindrically) radial part of the wave vector. | ||
| The cylindrically radial part is defined by :math:`k_\rho = \sqrt(k^2 - k_z^2)`. | ||
| In case of chiral materials :math:`k` and so :math`k_\rho` depends on the | ||
| polarization. The returned values have non-negative imaginary parts. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| kz (float, array-like): z-component of the wave vector | ||
| pol (int, array-like): Polarization indices | ||
| (:ref:`params:Polarizations`). | ||
| Returns: | ||
| complex, array-like | ||
| """ | ||
| ks = self.ks(k0)[pol] | ||
| return misc.wave_vec_z(kz, 0, ks) | ||
| def kzs(self, k0, kx, ky, pol): | ||
| r"""The z-component of the wave vector. | ||
| The z-component of the wave vector is defined by | ||
| :math:`k_z = \sqrt(k^2 - k_x^2 - k_y^2)`. In case of chiral materials :math:`k` | ||
| and so :math`k_z` depends on the polarization. The returned values have | ||
| non-negative imaginary parts. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| kx (float, array-like): x-component of the wave vector | ||
| ky (float, array-like): y-component of the wave vector | ||
| pol (int, array-like): Polarization indices | ||
| (:ref:`params:Polarizations`). | ||
| Returns: | ||
| complex, array-like | ||
| """ | ||
| ks = self.ks(k0)[pol] | ||
| return misc.wave_vec_z(kx, ky, ks) |
Sorry, the diff of this file is too big to display
| import copy | ||
| import numpy as np | ||
| from treams import config, util | ||
| from treams._core import PhysicsArray | ||
| from treams._core import PlaneWaveBasisByComp as PWBC | ||
| from treams._material import Material | ||
| from treams._operators import translate | ||
| from treams._tmatrix import TMatrixC | ||
| from treams.coeffs import fresnel | ||
| class SMatrix(PhysicsArray): | ||
| """S-matrix for a plane wave.""" | ||
| def _check(self): | ||
| super()._check() | ||
| shape = np.shape(self) | ||
| if len(shape) != 2 or shape[0] != shape[1]: | ||
| raise util.AnnotationError(f"invalid shape: '{shape}'") | ||
| if not isinstance(self.k0, (int, float, np.floating, np.integer)): | ||
| raise util.AnnotationError("invalid k0") | ||
| if self.poltype is None: | ||
| self.poltype = config.POLTYPE | ||
| if self.poltype not in ("parity", "helicity"): | ||
| raise util.AnnotationError("invalid poltype") | ||
| material = (None, None) if self.material is None else self.material | ||
| if isinstance(material, tuple): | ||
| self.material = tuple(Material() if m is None else m for m in material) | ||
| if self.basis is None: | ||
| raise util.AnnotationError("basis not set") | ||
| if not isinstance(self.basis, PWBC): | ||
| self.basis = self.basis.bycomp(self.k0, self.material) | ||
| class OperatorAttributeSMatrices: | ||
| def __init__(self, name): | ||
| self._name = name | ||
| self._obj = self._objtype = None | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| self._objtype = type(obj) if objtype is None else objtype | ||
| return self | ||
| def __call__(self, *args, **kwargs): | ||
| res = [ | ||
| [getattr(ii, self._name)(*args, **kwargs) for ii in i] for i in self._obj | ||
| ] | ||
| return self._objtype(res) | ||
| def apply_left(self, *args, **kwargs): | ||
| res = [ | ||
| [getattr(ii, self._name).apply_left(*args, **kwargs) for ii in i] | ||
| for i in self._obj | ||
| ] | ||
| return self._objtype(res) | ||
| def apply_right(self, *args, **kwargs): | ||
| res = [ | ||
| [getattr(ii, self._name).apply_right(*args, **kwargs) for ii in i] | ||
| for i in self._obj | ||
| ] | ||
| return self._objtype(res) | ||
| class SMatrices: | ||
| r"""Collection of four S-matrices with a plane wave basis. | ||
| The S-matrix describes the scattering of incoming into outgoing modes using a plane | ||
| wave basis, with functions :func:`treams.special.vsw_A`, | ||
| :func:`treams.special.vsw_M`, and :func:`treams.special.vsw_N`. The primary | ||
| direction of propagation is parallel or anti-parallel to the z-axis. The scattering | ||
| object itself is infinitely extended in the x- and y-directions. The S-matrix is | ||
| divided into four submatrices :math:`S_{\uparrow \uparrow}`, | ||
| :math:`S_{\uparrow \downarrow}`, :math:`S_{\downarrow \uparrow}`, and | ||
| :math:`S_{\downarrow \downarrow}`: | ||
| .. math:: | ||
| S = \begin{pmatrix} | ||
| S_{\uparrow \uparrow} & S_{\uparrow \downarrow} \\ | ||
| S_{\downarrow \uparrow} & S_{\downarrow \downarrow} | ||
| \end{pmatrix}\,. | ||
| These matrices describe the transmission of plane waves propagating into positive | ||
| z-direction, reflection of plane waves into the positive z-direction, reflection of | ||
| plane waves into negative z-direction, and the transmission of plane waves | ||
| propagating in negative z-direction, respectively. Each of those for matrices | ||
| contain different diffraction orders and different polarizations. The polarizations | ||
| can be in either helicity of parity basis. | ||
| The wave number :attr:`k0` and, if not vacuum, the material :attr:`material` are | ||
| also required. | ||
| Args: | ||
| smats (SMatrix): S-matrices. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (PlaneWaveBasisByComp): The basis for the S-matrices. | ||
| material (tuple, Material, optional): Material definition, if a tuple of length | ||
| two is specified, this refers to the materials above and below the S-matrix. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| """ | ||
| permute = OperatorAttributeSMatrices("permute") | ||
| translate = OperatorAttributeSMatrices("translate") | ||
| rotate = OperatorAttributeSMatrices("rotate") | ||
| changepoltype = OperatorAttributeSMatrices("changepoltype") | ||
| def __init__(self, smats, **kwargs): | ||
| """Initialization.""" | ||
| if len(smats) != 2 or len(smats[0]) != 2 or len(smats[1]) != 2: | ||
| raise ValueError("invalid shape of S-matrices") | ||
| if "material" in kwargs: | ||
| material = kwargs["material"] | ||
| del kwargs["material"] | ||
| elif hasattr(smats[0][0], "material"): | ||
| material = smats[0][0].material | ||
| elif hasattr(smats[1][1], "material"): | ||
| material = smats[1][1].material | ||
| if isinstance(material, tuple): | ||
| material = material[::-1] | ||
| elif hasattr(smats[0][1], "material"): | ||
| material = smats[0][1].material | ||
| elif hasattr(smats[1][0], "material"): | ||
| material = smats[1][0].material | ||
| else: | ||
| material = Material() | ||
| if isinstance(material, tuple): | ||
| ma, mb = material | ||
| else: | ||
| ma = mb = Material(material) | ||
| material = [[(ma, mb), (ma, ma)], [(mb, mb), (mb, ma)]] | ||
| modetype = [[("up", "up"), ("up", "down")], [("down", "up"), ("down", "down")]] | ||
| self._sms = [ | ||
| [ | ||
| SMatrix(s, material=m, modetype=t, **kwargs) | ||
| for s, m, t in zip(ar, mr, tr) | ||
| ] | ||
| for ar, mr, tr in zip(smats, material, modetype) | ||
| ] | ||
| self.material = self[0, 0].material | ||
| self.basis = self[0, 0].basis | ||
| self.k0 = self[0, 0].k0 | ||
| self.poltype = self[0, 0].poltype | ||
| for i in (self[1, 0], self[0, 1], self[1, 1]): | ||
| i.k0 = self.k0 | ||
| i.basis = self.basis | ||
| i.poltype = self.poltype | ||
| def __eq__(self, other): | ||
| return all( | ||
| (np.abs(self[i, j] - other[i][j]) < 1e-14).all() | ||
| for i in range(2) | ||
| for j in range(2) | ||
| ) | ||
| def __getitem__(self, key): | ||
| keys = {0: 0, "up": 0, 1: 1, "down": 1} | ||
| if key in keys: | ||
| return self._sms[keys[key]] | ||
| if not isinstance(key, tuple) or len(key) not in (1, 2): | ||
| raise KeyError(f"invalid key: '{key}'") | ||
| if len(key) == 1: | ||
| return self[key[0]] | ||
| key = tuple(keys[k] for k in key) | ||
| return self._sms[key[0]][key[1]] | ||
| def __len__(self): | ||
| return 2 | ||
| def __iter__(self): | ||
| return iter(self._sms) | ||
| @classmethod | ||
| def interface(cls, basis, k0, materials, poltype=None): | ||
| """Planar interface between two media. | ||
| Args: | ||
| basis (PlaneWaveBasisByComp): Basis definitions. | ||
| k0 (float): Wave number in vacuum | ||
| materials (Sequence[Material]): Material definitions. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| materials = tuple(map(Material, materials)) | ||
| ks = np.array([m.ks(k0) for m in materials]) | ||
| choice = basis.pol == 0 | ||
| kxs = basis.kx[choice], basis.kx[~choice] | ||
| kys = basis.ky[choice], basis.ky[~choice] | ||
| qs = np.zeros((2, 2, len(basis), len(basis)), complex) | ||
| if all(kxs[0] == kxs[1]) and all(kys[0] == kys[1]): | ||
| kzs = np.stack( | ||
| [ | ||
| m.kzs(k0, kxs[0][:, None], kys[0][:, None], np.array([[0, 1]])) | ||
| for m in materials | ||
| ], | ||
| -2, | ||
| ) | ||
| vals = fresnel(ks, kzs, [m.impedance for m in materials]) | ||
| qs[:, :, choice, choice] = np.moveaxis(vals[:, :, :, 0, 0], 0, -1) | ||
| qs[:, :, choice, ~choice] = np.moveaxis(vals[:, :, :, 0, 1], 0, -1) | ||
| qs[:, :, ~choice, choice] = np.moveaxis(vals[:, :, :, 1, 0], 0, -1) | ||
| qs[:, :, ~choice, ~choice] = np.moveaxis(vals[:, :, :, 1, 1], 0, -1) | ||
| else: | ||
| for i, (kx, ky, pol) in enumerate(basis): | ||
| for ii, (kx2, ky2, pol2) in enumerate(basis): | ||
| if kx != kx2 or ky != ky2: | ||
| continue | ||
| kzs = np.array([m.kzs(k0, kx, ky) for m in materials]) | ||
| vals = fresnel(ks, kzs, [m.impedance for m in materials]) | ||
| qs[:, :, ii, i] = vals[:, :, pol2, pol] | ||
| res = cls(qs, k0=k0, basis=basis, material=materials[::-1], poltype="helicity") | ||
| if poltype == "helicity": | ||
| return res | ||
| res = res.changepoltype(poltype) | ||
| res[0, 0][~np.eye(len(res), dtype=bool)] = 0 | ||
| res[0, 1][~np.eye(len(res), dtype=bool)] = 0 | ||
| res[1, 0][~np.eye(len(res), dtype=bool)] = 0 | ||
| res[1, 1][~np.eye(len(res), dtype=bool)] = 0 | ||
| return res | ||
| @classmethod | ||
| def slab(cls, thickness, basis, k0, materials, poltype=None): | ||
| """Slab of material. | ||
| A slab of material, defined by a thickness and three materials. Consecutive | ||
| slabs of material can be defined by `n` thicknesses and and `n + 2` material | ||
| parameters. | ||
| Args: | ||
| k0 (float): Wave number in vacuum. | ||
| basis (PlaneWaveBasisByComp): Basis definition. | ||
| tickness (Sequence[Float]): Thickness of the slab or the thicknesses of all | ||
| slabs in order from negative to positive z. | ||
| materials (Sequenze[Material]): Material definitions from negative to | ||
| positive z. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| try: | ||
| iter(thickness) | ||
| except TypeError: | ||
| thickness = [thickness] | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| res = cls.interface(basis, k0, materials[:2], poltype) | ||
| for d, ma, mb in zip(thickness, materials[1:-1], materials[2:]): | ||
| if np.ndim(d) == 0: | ||
| d = [0, 0, d] | ||
| x = cls.propagation(d, basis, k0, ma, poltype) | ||
| res = res.add(x) | ||
| res = res.add(cls.interface(basis, k0, (ma, mb), poltype)) | ||
| return res | ||
| @classmethod | ||
| def stack(cls, items): | ||
| """Stack of S-matrices. | ||
| Electromagnetically couple multiple S-matrices in the order given. Before | ||
| coupling it can be checked for matching materials and modes. | ||
| Args: | ||
| items (Sequence[SMatrix]): An array of S-matrices in their intended order | ||
| from negative to positive z. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| acc = items[0] | ||
| for item in items[1:]: | ||
| acc = acc.add(item) | ||
| return acc | ||
| @classmethod | ||
| def propagation(cls, r, basis, k0, material=Material(), poltype=None): | ||
| """S-matrix for the propagation along a distance. | ||
| This S-matrix translates the reference origin along `r`. | ||
| Args: | ||
| r (float, (3,)-array): Translation vector. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (PlaneWaveBasis): Basis definition. | ||
| material (Material, optional): Material definition. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| sup = translate( | ||
| r, basis=basis, k0=k0, material=material, modetype="up", poltype=poltype | ||
| ) | ||
| sdown = translate( | ||
| np.negative(r), | ||
| basis=basis, | ||
| k0=k0, | ||
| material=material, | ||
| modetype="down", | ||
| poltype=poltype, | ||
| ) | ||
| zero = np.zeros_like(sup) | ||
| material = Material(material) | ||
| return cls( | ||
| [[sup, zero], [zero, sdown]], | ||
| basis=basis, | ||
| k0=k0, | ||
| material=material, | ||
| poltype=poltype, | ||
| ) | ||
| @classmethod | ||
| def from_array(cls, tm, basis): | ||
| """S-matrix from an array of (cylindrical) T-matrices. | ||
| Create a S-matrix for a two-dimensional array of objects described by the | ||
| T-Matrix or an one-dimensional array of objects described by a cylindrical | ||
| T-matrix. | ||
| Args: | ||
| tm (TMatrix or TMatrixC): (Cylindrical) T-matrix to place in the array. | ||
| basis (PlaneWaveBasisByComp): Basis definition. | ||
| eta (float or complex, optional): Splitting parameter in the lattice sum. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| if isinstance(tm, TMatrixC): | ||
| basis = basis.permute(-1) | ||
| pu, pd = (tm.expandlattice(basis=basis, modetype=i) for i in ("up", "down")) | ||
| au, ad = (tm.expand.eval_inv(basis, i) for i in ("up", "down")) | ||
| eye = np.eye(len(basis)) | ||
| res = cls([[eye + pu @ au, pu @ ad], [pd @ au, eye + pd @ ad]]) | ||
| if isinstance(tm, TMatrixC): | ||
| res = res.permute() | ||
| return res | ||
| def add(self, sm): | ||
| """Couple another S-matrix on top of the current one. | ||
| See also :func:`treams.SMatrix.stack` for a function that does not change the | ||
| current S-matrix but creates a new one. | ||
| Args: | ||
| sm (SMatrix): S-matrix to add. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| dim = len(self.basis) | ||
| snew = [[None, None], [None, None]] | ||
| s_tmp = np.linalg.solve(np.eye(dim) - self[0, 1] @ sm[1][0], self[0, 0]) | ||
| snew[0][0] = sm[0][0] @ s_tmp | ||
| snew[1][0] = self[1, 0] + self[1, 1] @ sm[1][0] @ s_tmp | ||
| s_tmp = np.linalg.solve(np.eye(dim) - sm[1][0] @ self[0, 1], sm[1][1]) | ||
| snew[1][1] = self[1, 1] @ s_tmp | ||
| snew[0][1] = sm[0][1] + sm[0][0] @ self[0, 1] @ s_tmp | ||
| return SMatrices(snew) | ||
| def double(self, n=1): | ||
| """Double the S-matrix. | ||
| By default this function doubles the S-matrix but it can also create a | ||
| :math:`2^n`-fold repetition of itself: | ||
| Args: | ||
| n (int, optional): Number of times to double itself. Defaults to 1. | ||
| Returns: | ||
| SMatrix | ||
| """ | ||
| res = self | ||
| for _ in range(n): | ||
| res = res.add(res) | ||
| return res | ||
| def illuminate(self, illu, illu2=None, /, *, smat=None): | ||
| """Field coefficients above and below the S-matrix. | ||
| Given an illumination defined by the coefficients of each incoming mode | ||
| calculate the coefficients for the outgoing field above and below the S-matrix. | ||
| If a second SMatrix is given, the field expansions in between are also | ||
| calculated. | ||
| Args: | ||
| illu (array-like): Illumination, if `modetype` is specified, the direction | ||
| will be chosen accordingly. | ||
| illu2 (array-like, optional): Second illumination. If used, the first | ||
| argument is taken to be coming from below and this one to be coming from | ||
| above. | ||
| smat (SMatrix, optional): Second S-matrix for the calculation of the | ||
| field expansion between two S-matrices. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| modetype = getattr(illu, "modetype", "up") | ||
| if isinstance(modetype, tuple): | ||
| modetype = modetype[max(-2, -len(tuple))] | ||
| illu2 = np.zeros(np.shape(illu)[-2:]) if illu2 is None else illu2 | ||
| if modetype == "down": | ||
| illu, illu2 = illu2, illu | ||
| if smat is None: | ||
| field_up = self[0, 0] @ illu + self[0, 1] @ illu2 | ||
| field_down = self[1, 0] @ illu + self[1, 1] @ illu2 | ||
| return field_up, field_down | ||
| stmp = np.eye(len(self.basis)) - self[0, 1] @ smat[1, 0] | ||
| field_in_up = np.linalg.solve( | ||
| stmp, self[0, 0] @ illu + self[0, 1] @ smat[1, 1] @ illu2 | ||
| ) | ||
| field_in_down = smat[1, 0] @ field_in_up + smat[1, 1] @ illu2 | ||
| field_up = smat[0, 1] @ illu2 + smat[0, 0] @ field_in_up | ||
| field_down = self[1, 0] @ illu + self[1, 1] @ field_in_down | ||
| return field_up, field_down, field_in_up, field_in_down | ||
| def tr(self, illu): | ||
| """Transmittance and reflectance. | ||
| Calculate the transmittance and reflectance for one S-matrix with the given | ||
| illumination and direction. | ||
| Args: | ||
| illu (complex, array_like): Expansion coefficients for the incoming light | ||
| Returns: | ||
| tuple | ||
| """ | ||
| modetype = getattr(illu, "modetype", "up") | ||
| if isinstance(modetype, tuple): | ||
| modetype = modetype[max(-2, -len(tuple))] | ||
| trans, refl = self.illuminate(illu) | ||
| material = self.material | ||
| if not isinstance(material, tuple): | ||
| material = material, material | ||
| paz = [poynting_avg_z(self.basis, self.k0, m, self.poltype) for m in material] | ||
| if modetype == "down": | ||
| trans, refl = refl, trans | ||
| paz.reverse() | ||
| illu = np.asarray(illu) | ||
| s_t = np.real(trans.conjugate().T @ paz[0][0] @ trans) | ||
| s_r = np.real(refl.conjugate().T @ paz[1][0] @ refl) | ||
| s_i = np.real(np.conjugate(illu).T @ paz[1][0] @ illu) | ||
| s_ir = np.real( | ||
| refl.conjugate().T @ paz[1][1] @ illu | ||
| - np.conjugate(illu).T @ paz[1][1] @ refl | ||
| ) | ||
| return s_t / (s_i + s_ir), s_r / (s_i + s_ir) | ||
| def cd(self, illu): | ||
| """Transmission and absorption circular dichroism. | ||
| Calculate the transmission and absorption CD for one S-matrix with the given | ||
| illumination and direction. | ||
| Args: | ||
| illu (complex, PhysicsArray): Expansion coefficients for the incoming light | ||
| Returns: | ||
| tuple | ||
| """ | ||
| minus, plus = self.basis.pol == 0, self.basis.pol == 1 | ||
| if self.poltype == "helicity": | ||
| illuopposite = np.zeros_like(illu) | ||
| illuopposite[minus] = illu[plus] | ||
| illuopposite[plus] = illu[minus] | ||
| else: | ||
| illuopposite = copy.deepcopy(illu) | ||
| illuopposite[minus] *= -1 | ||
| tm, rm = self.tr(illu) | ||
| tp, rp = self.tr(illuopposite) | ||
| return (tp - tm) / (tp + tm), (tp + rp - tm - rm) / (tp + rp + tm + rm) | ||
| def periodic(self): | ||
| r"""Periodic repetition of the S-matrix. | ||
| Transform the S-matrix to an infinite periodic arrangement of itself defined | ||
| by | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| S_{\uparrow \uparrow} & S_{\uparrow \downarrow} \\ | ||
| -S_{\downarrow \downarrow}^{-1} | ||
| S_{\downarrow \uparrow} S_{\uparrow \uparrow} & | ||
| S_{\downarrow \downarrow}^{-1} (\mathbb{1} - S_{\downarrow \uparrow} | ||
| S_{\uparrow \downarrow}) | ||
| \end{pmatrix}\,. | ||
| Returns: | ||
| complex, array_like | ||
| """ | ||
| dim = len(self.basis) | ||
| res = np.empty((2 * dim, 2 * dim), dtype=complex) | ||
| res[0:dim, 0:dim] = self[0, 0] | ||
| res[0:dim, dim:] = self[0, 1] | ||
| res[dim:, 0:dim] = -np.linalg.solve(self[1, 1], self[1, 0] @ self[0, 0]) | ||
| res[dim:, dim:] = np.linalg.solve( | ||
| self[1, 1], np.eye(dim) - self[1, 0] @ self[0, 1] | ||
| ) | ||
| return res | ||
| def bands_kz(self, az): | ||
| r"""Band structure calculation. | ||
| Calculate the band structure for the given S-matrix, assuming it is periodically | ||
| repeated along the z-axis. The function returns the z-components of the wave | ||
| vector :math:`k_z` and the corresponding eigenvectors :math:`v` of | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| S_{\uparrow \uparrow} & S_{\uparrow \downarrow} \\ | ||
| -S_{\downarrow \downarrow}^{-1} S_{\downarrow \uparrow} | ||
| S_{\uparrow \uparrow} & | ||
| S_{\downarrow \downarrow}^{-1} (\mathbb{1} - S_{\downarrow \uparrow} | ||
| S_{\uparrow \downarrow}) | ||
| \end{pmatrix} | ||
| \boldsymbol v | ||
| = | ||
| \mathrm{e}^{\mathrm{i}k_z a_z} | ||
| \boldsymbol v\,. | ||
| Args: | ||
| az (float): Lattice pitch along the z direction | ||
| Returns: | ||
| tuple | ||
| """ | ||
| w, v = np.linalg.eig(self.periodic()) | ||
| return -1j * np.log(w) / az, v | ||
| def __repr__(self): | ||
| return f"""{type(self).__name__}(..., | ||
| basis={self.basis}, | ||
| k0={self.k0}, | ||
| material={self.material}, | ||
| poltype='{self.poltype}', | ||
| )""" | ||
| def chirality_density(basis, k0, material=Material(), poltype=None, z=(0, 0)): | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| material = Material(material) | ||
| kx, ky, kz = basis.kvecs(k0, material) | ||
| k = material.ks(k0)[basis.pol] | ||
| re, im = np.real(kz), np.imag(kz) | ||
| prefuu = 1 + np.real((k * k - kz * 2j * im) / (k * k.conjugate())) | ||
| prefdu = 1 + np.real((k * k - kz * 2 * re) / (k * k.conjugate())) | ||
| if np.abs(z[1] - z[0]) > 1e-16: | ||
| prefuu *= np.sinh(re * (z[1] - z[0])) / (re * (z[1] - z[0])) | ||
| prefdd = np.exp(im * (z[1] + z[0])) * prefuu | ||
| prefuu *= np.exp(-im * (z[1] + z[0])) | ||
| prefdu *= ( | ||
| np.sin(re * (z[1] - z[0])) | ||
| * np.cos(re * (z[1] + z[0])) | ||
| / (re * (z[1] - z[0])) | ||
| ) | ||
| else: | ||
| prefdd = prefuu | ||
| if poltype == "helicity": | ||
| return ( | ||
| np.diag(prefuu * (2 * basis.pol - 1)), | ||
| np.diag(prefdd * (2 * basis.pol - 1)), | ||
| np.diag(2 * prefdu * (2 * basis.pol - 1)), | ||
| ) | ||
| if poltype == "parity": | ||
| where = ( | ||
| (kx[:, None] == kx) | ||
| & (ky[:, None] == ky) | ||
| & (basis.pol[:, None] != basis.pol) | ||
| ) | ||
| return where * prefuu, where * prefdd, where * 2 * prefdu | ||
| raise ValueError(f"invalid poltype: '{poltype}'") | ||
| def poynting_avg_z(basis, k0, material=Material(), poltype=None): | ||
| r"""Time-averaged z-component of the Poynting vector. | ||
| Calculate the time-averaged Poynting vector's z-component | ||
| .. math:: | ||
| \langle S_z \rangle | ||
| = \frac{1}{2} | ||
| \Re (\boldsymbol E \times \boldsymbol H^\ast) \boldsymbol{\hat{z}} | ||
| on one side of the S-matrix with the given coefficients. | ||
| Returns: | ||
| tuple | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| material = Material(material) | ||
| kx, ky, kzs = basis.kvecs(k0, material) | ||
| gamma = kzs / (material.ks(k0)[basis.pol] * material.impedance) | ||
| selection = (kx[:, None] == kx) & (ky[:, None] == ky) | ||
| if poltype == "parity": | ||
| pol = basis.pol | ||
| selection = selection & (pol[:, None] == pol) | ||
| a = selection * ((1 - pol) * gamma.conjugate() + pol * gamma) * 0.25 | ||
| b = selection * ((1 - pol) * gamma.conjugate() - pol * gamma) * 0.25 | ||
| return a, b | ||
| if poltype == "helicity": | ||
| pol = 2 * basis.pol - 1 | ||
| a = selection * (pol[:, None] * pol * gamma[:, None].conjugate() + gamma) * 0.25 | ||
| b = selection * (pol[:, None] * pol * gamma[:, None].conjugate() - gamma) * 0.25 | ||
| return a, b | ||
| raise ValueError(f"invalid poltype: '{poltype}'") |
| import warnings | ||
| import numpy as np | ||
| import treams._operators as op | ||
| import treams.special as sc | ||
| from treams import config | ||
| from treams._core import CylindricalWaveBasis as CWB | ||
| from treams._core import PhysicsArray | ||
| from treams._core import PlaneWaveBasisByComp as PWBC | ||
| from treams._core import PlaneWaveBasisByUnitVector as PWBUV | ||
| from treams._core import SphericalWaveBasis as SWB | ||
| from treams._material import Material | ||
| from treams.coeffs import mie, mie_cyl | ||
| from treams.util import AnnotationError | ||
| class _Interaction: | ||
| def __init__(self): | ||
| self._obj = self._objtype = None | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| self._objtype = objtype | ||
| return self | ||
| def __call__(self): | ||
| basis = self._obj.basis | ||
| return ( | ||
| np.eye(self._obj.shape[-1]) - self._obj @ op.Expand(basis, "singular").inv | ||
| ) | ||
| def solve(self): | ||
| return np.linalg.solve(self(), self._obj) | ||
| class _LatticeInteraction: | ||
| def __init__(self): | ||
| self._obj = self._objtype = None | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| self._objtype = objtype | ||
| return self | ||
| def __call__(self, lattice, kpar, *, eta=0): | ||
| return np.eye(self._obj.shape[-1]) - self._obj @ op.ExpandLattice( | ||
| lattice=lattice, kpar=kpar, eta=eta | ||
| ) | ||
| def solve(self, lattice, kpar, *, eta=0): | ||
| return np.linalg.solve(self(lattice, kpar, eta=eta), self._obj) | ||
| class TMatrix(PhysicsArray): | ||
| """T-matrix with a spherical basis. | ||
| The T-matrix is square relating incident (regular) fields | ||
| :func:`treams.special.vsw_rA` (helical polarizations) or | ||
| :func:`treams.special.vsw_rN` and :func:`treams.special.vsw_rM` (parity | ||
| polarizations) to the corresponding scattered fields :func:`treams.special.vsw_A` or | ||
| :func:`treams.special.vsw_N` and :func:`treams.special.vsw_M`. The modes themselves | ||
| are defined in :attr:`basis`, the polarization type in :attr:`poltype`. Also, the | ||
| wave number :attr:`k0` and, if not vacuum, the material :attr:`material` are | ||
| specified. | ||
| Args: | ||
| arr (float or complex, array-like): T-matrix itself. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (SphericalWaveBasis, optional): Basis definition. | ||
| material (Material, optional): Embedding material, defaults to vacuum. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| lattice (Lattice, optional): Lattice definition. If specified the T-Matrix is | ||
| assumed to be periodically repeated in the defined lattice. | ||
| kpar (list, optional): Phase factor for the periodic T-Matrix. | ||
| """ | ||
| interaction = _Interaction() | ||
| latticeinteraction = _LatticeInteraction() | ||
| def _check(self): | ||
| """Fill in default values or raise errors for missing attributes.""" | ||
| super()._check() | ||
| shape = np.shape(self) | ||
| if len(shape) != 2 or shape[0] != shape[1]: | ||
| raise AnnotationError(f"invalid shape: '{shape}'") | ||
| if not isinstance(self.k0, (int, float, np.floating, np.integer)): | ||
| raise AnnotationError("invalid k0") | ||
| if self.poltype is None: | ||
| self.poltype = config.POLTYPE | ||
| if self.poltype not in ("parity", "helicity"): | ||
| raise AnnotationError("invalid poltype") | ||
| modetype = self.modetype | ||
| if modetype is None or ( | ||
| modetype[0] in (None, "singular") and modetype[1] in (None, "regular") | ||
| ): | ||
| self.modetype = ("singular", "regular") | ||
| else: | ||
| raise AnnotationError("invalid modetype") | ||
| if self.basis is None: | ||
| self.basis = SWB.default(SWB.defaultlmax(shape[0])) | ||
| if self.material is None: | ||
| self.material = Material() | ||
| @property | ||
| def ks(self): | ||
| """Wave numbers (in medium). | ||
| The wave numbers for both polarizations. | ||
| """ | ||
| return self.material.ks(self.k0) | ||
| @classmethod | ||
| def sphere(cls, lmax, k0, radii, materials, poltype=None): | ||
| """T-Matrix of a (multi-layered) sphere. | ||
| Construct the T-matrix of the given order and material for a sphere. The object | ||
| can also consist of multiple concentric spherical shells with an arbitrary | ||
| number of layers. The calculation is always done in helicity basis. | ||
| Args: | ||
| lmax (int): Positive integer for the maximum degree of the T-matrix. | ||
| k0 (float): Wave number in vacuum. | ||
| radii (float or array): Radii from inside to outside of the sphere. For a | ||
| simple sphere the radius can be given as a single number, for a multi- | ||
| layered sphere it is a list of increasing radii for all shells. | ||
| material (list[Material]): The material parameters from the inside to the | ||
| outside. The last material in the list specifies the embedding medium. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| Returns: | ||
| TMatrix | ||
| """ | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| materials = [Material(m) for m in materials] | ||
| radii = np.atleast_1d(radii) | ||
| if radii.size != len(materials) - 1: | ||
| raise ValueError("incompatible lengths of radii and materials") | ||
| dim = SWB.defaultdim(lmax) | ||
| tmat = np.zeros((dim, dim), np.complex128) | ||
| for l in range(1, lmax + 1): # noqa: E741 | ||
| miecoeffs = mie(l, k0 * radii, *zip(*materials)) | ||
| pos = SWB.defaultdim(l - 1) | ||
| for i in range(2 * l + 1): | ||
| tmat[ | ||
| pos + 2 * i : pos + 2 * i + 2, pos + 2 * i : pos + 2 * i + 2 | ||
| ] = miecoeffs[::-1, ::-1] | ||
| res = cls( | ||
| tmat, | ||
| k0=k0, | ||
| basis=SWB.default(lmax), | ||
| material=materials[-1], | ||
| poltype="helicity", | ||
| ) | ||
| if poltype == "helicity": | ||
| return res | ||
| res = res.changepoltype(poltype) | ||
| res[~np.eye(len(res), dtype=bool)] = 0 | ||
| return res | ||
| @classmethod | ||
| def cluster(cls, tmats, positions): | ||
| r"""Block-diagonal T-matrix of multiple objects. | ||
| Construct the initial block-diagonal T-matrix for a cluster of objects. The | ||
| T-matrices in the list are placed together into a block-diagonal matrix and the | ||
| complete (local) basis is defined based on the individual T-matrices and their | ||
| bases together with the defined positions. In mathematical terms the matrix | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| T_0 & 0 & \dots & 0 \\ | ||
| 0 & T_1 & \ddots & \vdots \\ | ||
| \vdots & \ddots & \ddots & 0 \\ | ||
| 0 & \dots & 0 & T_{N-1} \\ | ||
| \end{pmatrix} | ||
| is created from the list of T-matrices :math:`(T_0, \dots, T_{N-1})`. Only | ||
| T-matrices of the same wave number, embedding material, and polarization type | ||
| can be combined. | ||
| Args: | ||
| tmats (Sequence): List of T-matrices. | ||
| positions (array): The positions of all individual objects in the cluster. | ||
| Returns: | ||
| TMatrix | ||
| """ | ||
| for tm in tmats: | ||
| if not tm.basis.isglobal: | ||
| raise ValueError("global basis required") | ||
| positions = np.array(positions) | ||
| if len(tmats) < positions.shape[0]: | ||
| warnings.warn("specified more positions than T-matrices") | ||
| elif len(tmats) > positions.shape[0]: | ||
| raise ValueError( | ||
| f"'{len(tmats)}' T-matrices " | ||
| f"but only '{positions.shape[0]}' positions given" | ||
| ) | ||
| mat = tmats[0].material | ||
| k0 = tmats[0].k0 | ||
| poltype = tmats[0].poltype | ||
| modes = [], [], [] | ||
| pidx = [] | ||
| dim = sum(tmat.shape[0] for tmat in tmats) | ||
| tres = np.zeros((dim, dim), complex) | ||
| i = 0 | ||
| for j, tm in enumerate(tmats): | ||
| if tm.material != mat: | ||
| raise ValueError(f"incompatible materials: '{mat}' and '{tm.material}'") | ||
| if tm.k0 != k0: | ||
| raise ValueError(f"incompatible k0: '{k0}' and '{tm.k0}'") | ||
| if tm.poltype != poltype: | ||
| raise ValueError(f"incompatible modetypes: '{poltype}', '{tm.poltype}'") | ||
| dim = tm.shape[0] | ||
| for m, n in zip(modes, tm.basis.lms): | ||
| m.extend(list(n)) | ||
| pidx += [j] * dim | ||
| tres[i : i + dim, i : i + dim] = tm | ||
| i += dim | ||
| basis = SWB(zip(pidx, *modes), positions) | ||
| return cls(tres, k0=k0, material=mat, basis=basis, poltype=poltype) | ||
| @property | ||
| def isglobal(self): | ||
| """Test if a T-matrix is global. | ||
| A T-matrix is considered global, when its basis refers to only a single point | ||
| and it is not placed periodically in a lattice. | ||
| """ | ||
| return self.basis.isglobal and self.lattice is None and self.kpar is None | ||
| @property | ||
| def xs_ext_avg(self): | ||
| r"""Rotation and polarization averaged extinction cross section. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \sigma_\mathrm{ext} \rangle | ||
| = -2 \pi \sum_{slm} \frac{\Re(T_{slm,slm})}{k_s^2} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s`. It is only implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| if not self.material.ischiral: | ||
| k = self.ks[0] | ||
| res = -2 * np.pi * self.trace().real / (k * k) | ||
| else: | ||
| res = 0 | ||
| modetype = self.modetype | ||
| del self.modetype | ||
| diag = np.diag(self) | ||
| self.modetype = modetype | ||
| for pol in [0, 1]: | ||
| choice = self.basis.pol == pol | ||
| k = self.ks[pol] | ||
| res += -2 * np.pi * diag[choice].sum().real / (k * k) | ||
| if res.imag == 0: | ||
| return res.real | ||
| return res | ||
| @property | ||
| def xs_sca_avg(self): | ||
| r"""Rotation and polarization averaged scattering cross section. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \sigma_\mathrm{sca} \rangle | ||
| = 2 \pi \sum_{slm} \sum_{s'l'm'} | ||
| \frac{|T_{slm,s'l'm'}|^2}{k_s^2} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s`. It is only implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| if not self.material.ischiral: | ||
| ks = self.ks[0] | ||
| else: | ||
| ks = self.ks[self.basis.pol, None] | ||
| re, im = self.real, self.imag | ||
| res = 2 * np.pi * np.sum((re * re + im * im) / (ks * ks)) | ||
| return res.real | ||
| @property | ||
| def cd(self): | ||
| r"""Circular dichroism (CD). | ||
| The CD is calculated as | ||
| .. math:: | ||
| CD | ||
| = \frac{\langle \sigma_\mathrm{abs} \rangle_+ | ||
| - \langle \sigma_\mathrm{abs} \rangle_-} | ||
| {\langle \sigma_\mathrm{abs} \rangle_+ | ||
| + \langle \sigma_\mathrm{abs} \rangle_-} | ||
| where :math:`\langle \sigma \rangle_s` is the rotationally averaged absorption | ||
| cross section under the illumination with the polarization :math:`s`. | ||
| It is only implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not (self.isglobal and self.poltype == "helicity" and self.material.isreal): | ||
| raise NotImplementedError | ||
| sel = np.array(self.basis.pol, bool) | ||
| re, im = self.real, self.imag | ||
| plus = -np.sum(re.diagonal()[sel]) / (self.ks[1] * self.ks[1]) | ||
| re_part = re[:, sel] / self.ks[self.basis.pol, None] | ||
| im_part = im[:, sel] / self.ks[self.basis.pol, None] | ||
| plus -= np.sum(re_part * re_part + im_part * im_part) | ||
| sel = ~sel | ||
| minus = -np.sum(re.diagonal()[sel]) / (self.ks[0] * self.ks[0]) | ||
| re_part = re[:, sel] / self.ks[self.basis.pol, None] | ||
| im_part = im[:, sel] / self.ks[self.basis.pol, None] | ||
| minus -= np.sum(re_part * re_part + im_part * im_part) | ||
| return np.real((plus - minus) / (plus + minus)) | ||
| @property | ||
| def chi(self): | ||
| """Electromagnetic chirality. | ||
| Only implemented for global T-matrices. | ||
| Returns: | ||
| float | ||
| """ | ||
| if not (self.isglobal and self.poltype == "helicity"): | ||
| raise NotImplementedError | ||
| sel = self.basis.pol == 0, self.basis.pol == 1 | ||
| spp = np.linalg.svd(self[np.ix_(sel[1], sel[1])], compute_uv=False) | ||
| spm = np.linalg.svd(self[np.ix_(sel[1], sel[0])], compute_uv=False) | ||
| smp = np.linalg.svd(self[np.ix_(sel[0], sel[1])], compute_uv=False) | ||
| smm = np.linalg.svd(self[np.ix_(sel[0], sel[0])], compute_uv=False) | ||
| plus = np.concatenate((np.asarray(spp), np.asarray(spm))) | ||
| minus = np.concatenate((np.asarray(smm), np.asarray(smp))) | ||
| return np.linalg.norm(plus - minus) / np.sqrt(np.sum(np.power(np.abs(self), 2))) | ||
| @property | ||
| def db(self): | ||
| """Duality breaking. | ||
| Only implemented for global T-matrices. | ||
| Returns: | ||
| float | ||
| """ | ||
| if not (self.isglobal and self.poltype == "helicity"): | ||
| raise NotImplementedError | ||
| sel = self.basis.pol == 0, self.basis.pol == 1 | ||
| tpm = np.asarray(self[np.ix_(sel[1], sel[0])]) | ||
| tmp = np.asarray(self[np.ix_(sel[0], sel[1])]) | ||
| return np.sum( | ||
| tpm.real * tpm.real | ||
| + tpm.imag * tpm.imag | ||
| + tmp.real * tmp.real | ||
| + tmp.imag * tmp.imag | ||
| ) / (np.sum(np.power(np.abs(self), 2))) | ||
| def xs(self, illu, flux=0.5): | ||
| r"""Scattering and extinction cross section. | ||
| Possible for all T-matrices (global and local) in non-absorbing embedding. The | ||
| values are calculated by | ||
| .. math:: | ||
| \sigma_\mathrm{sca} | ||
| = \frac{1}{2 I} | ||
| a_{slm}^\ast T_{s'l'm',slm}^\ast k_{s'}^{-2} C_{s'l'm',s''l''m''}^{(1)} | ||
| T_{s''l''m'',s'''l'''m'''} a_{s'''l'''m'''} \\ | ||
| \sigma_\mathrm{ext} | ||
| = \frac{1}{2 I} | ||
| a_{slm}^\ast k_s^{-2} T_{slm,s'l'm'} a_{s'l'm'} | ||
| where :math:`a_{slm}` are the expansion coefficients of the illumination, | ||
| :math:`T` is the T-matrix, :math:`C^{(1)}` is the (regular) translation | ||
| matrix and :math:`k_s` are the wave numbers in the medium. All repeated indices | ||
| are summed over. The incoming flux is :math:`I`. | ||
| Args: | ||
| illu (complex, array): Illumination coefficients | ||
| flux (optional): Ingoing flux corresponding to the illumination. Used for | ||
| the result's normalization. The flux is given in units of | ||
| :math:`\frac{\text{V}^2}{{l^2}} \frac{1}{Z_0 Z}` where :math:`l` is the | ||
| unit of length used in the wave number (and positions). A plane wave | ||
| has the flux `0.5` in this normalization, which is used as default. | ||
| Returns: | ||
| tuple[float] | ||
| """ | ||
| if not self.material.isreal: | ||
| raise NotImplementedError | ||
| illu = PhysicsArray(illu) | ||
| illu_basis = illu.basis | ||
| illu_basis = illu_basis[-2] if isinstance(illu_basis, tuple) else illu_basis | ||
| if not isinstance(illu_basis, SWB): | ||
| illu = illu.expand(self.basis) | ||
| p = self @ illu | ||
| p_invksq = p * np.power(self.ks[self.basis.pol], -2) | ||
| del illu.modetype | ||
| return ( | ||
| 0.5 * np.real(p.conjugate().T @ p_invksq.expand(p.basis)) / flux, | ||
| -0.5 * np.real(illu.conjugate().T @ p_invksq) / flux, | ||
| ) | ||
| def valid_points(self, grid, radii): | ||
| """Points on the grid where the expansion is valid. | ||
| The expansion of the electromagnetic field is valid outside of the | ||
| circumscribing spheres of each object. From a given set of coordinates mark | ||
| those that are outside of the given radii. | ||
| Args: | ||
| grid (array-like): Points to assess. The last dimension needs length three | ||
| and corresponds to the Cartesian coordinates. | ||
| radii (Sequence[float]): Radii of the circumscribing spheres. Each radius | ||
| corresponds to a position of the basis. | ||
| Returns: | ||
| array | ||
| """ | ||
| grid = np.asarray(grid) | ||
| if grid.shape[-1] != 3: | ||
| raise ValueError("invalid grid") | ||
| if len(radii) != len(self.basis.positions): | ||
| raise ValueError("invalid length of 'radii'") | ||
| res = np.ones(grid.shape[:-1], bool) | ||
| for r, p in zip(radii, self.basis.positions): | ||
| res &= np.sum(np.power(grid - p, 2), axis=-1) > r * r | ||
| return res | ||
| def __getitem__(self, key): | ||
| if isinstance(key, SWB): | ||
| key = np.array([self.basis.index(i) for i in key]) | ||
| key = (key[:, None], key) | ||
| return super().__getitem__(key) | ||
| class TMatrixC(PhysicsArray): | ||
| """T-matrix with a cylindrical basis. | ||
| The T-matrix is square relating incident (regular) fields | ||
| :func:`treams.special.vcw_rA` (helical polarizations) or | ||
| :func:`treams.special.vcw_rN` and :func:`treams.special.vcw_rM` (parity | ||
| polarizations) to the corresponding scattered fields :func:`treams.special.vcw_A` or | ||
| :func:`treams.special.vcw_N` and :func:`treams.special.vcw_M`. The modes themselves | ||
| are defined in :attr:`basis`, the polarization type in :attr:`poltype`. Also, the | ||
| wave number :attr:`k0` and, if not vacuum, the material :attr:`material` are | ||
| specified. | ||
| Args: | ||
| arr (float or complex, array-like): T-matrix itself. | ||
| k0 (float): Wave number in vacuum. | ||
| basis (SphericalWaveBasis, optional): Basis definition. | ||
| material (Material, optional): Embedding material, defaults to vacuum. | ||
| poltype (str, optional): Polarization type (:ref:`params:Polarizations`). | ||
| lattice (Lattice, optional): Lattice definition. If specified the T-Matrix is | ||
| assumed to be periodically repeated in the defined lattice. | ||
| kpar (list, optional): Phase factor for the periodic T-Matrix. | ||
| """ | ||
| interaction = _Interaction() | ||
| latticeinteraction = _LatticeInteraction() | ||
| def _check(self): | ||
| """Fill in default values or raise errors for missing attributes.""" | ||
| super()._check() | ||
| shape = np.shape(self) | ||
| if len(shape) != 2 or shape[0] != shape[1]: | ||
| raise AnnotationError(f"invalid shape: '{shape}'") | ||
| if not isinstance(self.k0, (int, float, np.floating, np.integer)): | ||
| raise AnnotationError("invalid k0") | ||
| if self.poltype is None: | ||
| self.poltype = config.POLTYPE | ||
| if self.poltype not in ("parity", "helicity"): | ||
| raise AnnotationError("invalid poltype") | ||
| modetype = self.modetype | ||
| if modetype is None or ( | ||
| modetype[0] in (None, "singular") and modetype[1] in (None, "regular") | ||
| ): | ||
| self.modetype = ("singular", "regular") | ||
| else: | ||
| raise AnnotationError("invalid modetype") | ||
| if self.basis is None: | ||
| self.basis = CWB.default([0], CWB.defaultmmax(shape[0])) | ||
| if self.material is None: | ||
| self.material = Material() | ||
| @property | ||
| def ks(self): | ||
| """Wave numbers (in medium). | ||
| The wave numbers for both polarizations. | ||
| """ | ||
| return self.material.ks(self.k0) | ||
| @property | ||
| def krhos(self): | ||
| r"""Radial part of the wave. | ||
| Calculate :math:`\sqrt{k^2 - k_z^2}`, where :math:`k` is the wave number in the | ||
| medium for each illumination | ||
| Returns: | ||
| Sequence[complex] | ||
| """ | ||
| return self.material.krhos(self.k0, self.basis.kz, self.basis.pol) | ||
| @classmethod | ||
| def cylinder(cls, kzs, mmax, k0, radii, materials): | ||
| """T-Matrix of a (multi-layered) cylinder. | ||
| Construct the T-matrix of the given order and material for an infinitely | ||
| extended cylinder. The object can also consist of multiple concentric | ||
| cylindrical shells with an arbitrary number of layers. The calculation is always | ||
| done in helicity basis. | ||
| Args: | ||
| kzs (float, array_like): Z component of the cylindrical wave. | ||
| mmax (int): Positive integer for the maximum order of the T-matrix. | ||
| k0 (float): Wave number in vacuum. | ||
| radii (float or array): Radii from inside to outside of the cylinder. For a | ||
| simple cylinder the radius can be given as a single number, for a multi- | ||
| layered cylinder it is a list of increasing radii for all shells. | ||
| material (list[Material]): The material parameters from the inside to the | ||
| outside. The last material in the list specifies the embedding medium. | ||
| Returns: | ||
| T-Matrix object | ||
| """ | ||
| materials = [Material(m) for m in materials] | ||
| kzs = np.atleast_1d(kzs) | ||
| radii = np.atleast_1d(radii) | ||
| if radii.size != len(materials) - 1: | ||
| raise ValueError("incompatible lengths of radii and materials") | ||
| dim = CWB.defaultdim(len(kzs), mmax) | ||
| tmat = np.zeros((dim, dim), np.complex128) | ||
| idx = 0 | ||
| for kz in kzs: | ||
| for m in range(-mmax, mmax + 1): | ||
| miecoeffs = mie_cyl(kz, m, k0, radii, *zip(*materials)) | ||
| tmat[idx : idx + 2, idx : idx + 2] = miecoeffs[::-1, ::-1] | ||
| idx += 2 | ||
| return cls(tmat, k0=k0, basis=CWB.default(kzs, mmax), material=materials[-1]) | ||
| @classmethod | ||
| def cluster(cls, tmats, positions): | ||
| r"""Block-diagonal T-matrix of multiple objects. | ||
| Construct the initial block-diagonal T-matrix for a cluster of objects. The | ||
| T-matrices in the list are placed together into a block-diagonal matrix and the | ||
| complete (local) basis is defined based on the individual T-matrices and their | ||
| bases together with the defined positions. In mathematical terms the matrix | ||
| .. math:: | ||
| \begin{pmatrix} | ||
| T_0 & 0 & \dots & 0 \\ | ||
| 0 & T_1 & \ddots & \vdots \\ | ||
| \vdots & \ddots & \ddots & 0 \\ | ||
| 0 & \dots & 0 & T_{N-1} \\ | ||
| \end{pmatrix} | ||
| is created from the list of T-matrices :math:`(T_0, \dots, T_{N-1})`. Only | ||
| T-matrices of the same wave number, embedding material, and polarization type | ||
| can be combined. | ||
| Args: | ||
| tmats (Sequence): List of T-matrices. | ||
| positions (array): The positions of all individual objects in the cluster. | ||
| Returns: | ||
| TMatrix | ||
| """ | ||
| for tm in tmats: | ||
| if not tm.basis.isglobal: | ||
| raise ValueError("global basis required") | ||
| positions = np.array(positions) | ||
| if len(tmats) < positions.shape[0]: | ||
| warnings.warn("specified more positions than T-matrices") | ||
| elif len(tmats) > positions.shape[0]: | ||
| raise ValueError( | ||
| f"'{len(tmats)}' T-matrices " | ||
| f"but only '{positions.shape[0]}' positions given" | ||
| ) | ||
| mat = tmats[0].material | ||
| k0 = tmats[0].k0 | ||
| poltype = tmats[0].poltype | ||
| modes = [], [], [] | ||
| pidx = [] | ||
| dim = sum(tmat.shape[0] for tmat in tmats) | ||
| tres = np.zeros((dim, dim), complex) | ||
| i = 0 | ||
| for j, tm in enumerate(tmats): | ||
| if tm.material != mat: | ||
| raise ValueError(f"incompatible materials: '{mat}' and '{tm.material}'") | ||
| if tm.k0 != k0: | ||
| raise ValueError(f"incompatible k0: '{k0}' and '{tm.k0}'") | ||
| if tm.poltype != poltype: | ||
| raise ValueError(f"incompatible modetypes: '{poltype}', '{tm.poltype}'") | ||
| dim = tm.shape[0] | ||
| for m, n in zip(modes, tm.basis.zms): | ||
| m.extend(list(n)) | ||
| pidx += [j] * dim | ||
| tres[i : i + dim, i : i + dim] = tm | ||
| i += dim | ||
| basis = CWB(zip(pidx, *modes), positions) | ||
| return cls(tres, k0=k0, material=mat, basis=basis, poltype=poltype) | ||
| @classmethod | ||
| def from_array(cls, tm, basis, *, eta=0): | ||
| """1d array of spherical T-matrices.""" | ||
| return cls( | ||
| (tm @ op.Expand(basis).inv).expandlattice(basis=basis, eta=eta), | ||
| lattice=tm.lattice, | ||
| kpar=tm.kpar, | ||
| ) | ||
| @property | ||
| def xw_ext_avg(self): | ||
| r"""Rotation and polarization averaged extinction cross width. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \lambda_\mathrm{ext} \rangle | ||
| = -\frac{2 \pi}{n_{k_z}} \sum_{sk_zm} \frac{\Re(T_{sk_zm,sk_zm})}{k_s} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s` and :math:`n_{k_z}` is the number of wave components | ||
| :math:`k_z` included in the T-matrix. The average is taken over all given | ||
| z-components of the wave vector and rotations around the z-axis. It is only | ||
| implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| nk = np.unique(self.basis.kz).size | ||
| if not self.material.ischiral: | ||
| res = -2 * np.real(np.trace(self)) / (self.ks[0] * nk) | ||
| else: | ||
| res = 0 | ||
| modetype = self.modetype | ||
| del self.modetype | ||
| diag = np.diag(self) | ||
| self.modetype = modetype | ||
| for pol in [0, 1]: | ||
| choice = self.basis.pol == pol | ||
| res += -2 * np.real(diag[choice].sum()) / (self.ks[pol] * nk) | ||
| if res.imag == 0: | ||
| return res.real | ||
| return res | ||
| @property | ||
| def xw_sca_avg(self): | ||
| r"""Rotation and polarization averaged scattering cross width. | ||
| The average is calculated as | ||
| .. math:: | ||
| \langle \lambda_\mathrm{sca} \rangle | ||
| = \frac{2 \pi}{n_{k_z}} \sum_{sk_zm} \sum_{s'{k_z}'m'} | ||
| \frac{|T_{sk_zm,s'{k_z}'m'}|^2}{k_s} | ||
| where :math:`k_s` is the wave number in the embedding medium for the | ||
| polarization :math:`s`. and :math:`n_{k_z}` is the number of wave components | ||
| :math:`k_z` included in the T-matrix. The average is taken over all given | ||
| z-components of the wave vector and rotations around the z-axis. It is only | ||
| implemented for global T-matrices. | ||
| Returns: | ||
| float or complex | ||
| """ | ||
| if not self.isglobal or not self.material.isreal: | ||
| raise NotImplementedError | ||
| if not self.material.ischiral: | ||
| ks = self.ks[0] | ||
| else: | ||
| ks = self.ks[self.basis.pol, None] | ||
| re, im = self.real, self.imag | ||
| nk = np.unique(self.basis.kz).size | ||
| res = 2 * np.sum((re * re + im * im) / (ks * nk)) | ||
| return res.real | ||
| @property | ||
| def isglobal(self): | ||
| """Test if a T-matrix is global. | ||
| A T-matrix is considered global, when its basis refers to only a single point | ||
| and it is not placed periodically in a lattice. | ||
| """ | ||
| return self.basis.isglobal and self.lattice is None and self.kpar is None | ||
| def xw(self, illu, flux=0.5): | ||
| r"""Scattering and extinction cross width. | ||
| Possible for all T-matrices (global and local) in non-absorbing embedding. The | ||
| values are calculated by | ||
| .. math:: | ||
| \lambda_\mathrm{sca} | ||
| = \frac{1}{2 I} | ||
| a_{sk_zm}^\ast T_{s'{k_z}'m',sk_zm}^\ast k_{s'}^{-2} | ||
| C_{s'l'm',s''l''m''}^{(1)} | ||
| T_{s''l''m'',s'''l'''m'''} a_{s'''l'''m'''} \\ | ||
| \sigma_\mathrm{ext} | ||
| = \frac{1}{2 I} | ||
| a_{slm}^\ast k_s^{-2} T_{slm,s'l'm'} a_{s'l'm'} | ||
| where :math:`a_{slm}` are the expansion coefficients of the illumination, | ||
| :math:`T` is the T-matrix, :math:`C^{(1)}` is the (regular) translation | ||
| matrix and :math:`k_s` are the wave numbers in the medium. All repeated indices | ||
| are summed over. The incoming flux is :math:`I`. | ||
| Args: | ||
| illu (complex, array): Illumination coefficients | ||
| flux (optional): Ingoing flux corresponding to the illumination. Used for | ||
| the result's normalization. The flux is given in units of | ||
| :math:`\frac{\text{V}^2}{{l^2}} \frac{1}{Z_0 Z}` where :math:`l` is the | ||
| unit of length used in the wave number (and positions). A plane wave | ||
| has the flux `0.5` in this normalization, which is used as default. | ||
| Returns: | ||
| tuple[float] | ||
| """ | ||
| if not self.material.isreal: | ||
| raise NotImplementedError | ||
| illu = PhysicsArray(illu) | ||
| illu_basis = illu.basis | ||
| illu_basis = illu_basis[-2] if isinstance(illu_basis, tuple) else illu_basis | ||
| if not isinstance(illu_basis, CWB): | ||
| illu = illu.expand(self.basis) | ||
| p = self @ illu | ||
| p_invk = p / self.ks[self.basis.pol] | ||
| del illu.modetype | ||
| return ( | ||
| 2 * np.real(p.conjugate().T @ p_invk.expand(p.basis)) / flux, | ||
| -2 * np.real(illu.conjugate().T @ p_invk) / flux, | ||
| ) | ||
| def valid_points(self, grid, radii): | ||
| grid = np.asarray(grid) | ||
| if grid.shape[-1] not in (2, 3): | ||
| raise ValueError("invalid grid") | ||
| if len(radii) != len(self.basis.positions): | ||
| raise ValueError("invalid length of 'radii'") | ||
| res = np.ones(grid.shape[:-1], bool) | ||
| for r, p in zip(radii, self.basis.positions): | ||
| res &= np.sum(np.power(grid[..., :2] - p[:2], 2), axis=-1) > r * r | ||
| return res | ||
| def __getitem__(self, key): | ||
| if isinstance(key, CWB): | ||
| key = np.array([self.basis.index(i) for i in key]) | ||
| key = (key[:, None], key) | ||
| return super().__getitem__(key) | ||
| def _plane_wave_partial( | ||
| kpar, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| if basis is None: | ||
| basis = PWBC.default([kpar]) | ||
| if pol in (0, -1): | ||
| pol = [1, 0] | ||
| elif pol == 1: | ||
| pol = [0, 1] | ||
| elif len(pol) == 3: | ||
| modetype = "up" if modetype is None else modetype | ||
| if modetype not in ("up", "down"): | ||
| raise ValueError(f"invalid 'modetype': {modetype}") | ||
| kvecs = np.array(basis.kvecs(k0, material, modetype)) | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| if poltype == "parity": | ||
| kvec = kvecs[:, basis.index((kpar[0], kpar[1], 0))] | ||
| pol = [ | ||
| -sc.vpw_M(*kvec, 0, 0, 0) @ pol, | ||
| sc.vpw_N(*kvec, 0, 0, 0) @ pol, | ||
| ] | ||
| elif poltype == "helicity": | ||
| kvec = kvecs[ | ||
| :, | ||
| [ | ||
| basis.index((kpar[0], kpar[1], 1)), | ||
| basis.index((kpar[0], kpar[1], 0)), | ||
| ], | ||
| ] | ||
| pol = sc.vpw_A(*kvec, 0, 0, 0, [1, 0]) @ pol | ||
| else: | ||
| raise ValueError(f"invalid 'poltype': {poltype}") | ||
| res = [pol[x[2]] * (np.abs(np.array(kpar) - x[:2]) < 1e-14).all() for x in basis] | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) | ||
| def _plane_wave( | ||
| kvec, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| if basis is None: | ||
| basis = PWBUV.default([kvec]) | ||
| norm = np.sqrt(np.sum(np.power(kvec, 2))) | ||
| qvec = kvec / norm | ||
| if pol in (0, -1): | ||
| pol = [1, 0] | ||
| elif pol == 1: | ||
| pol = [0, 1] | ||
| elif len(pol) == 3: | ||
| if None not in (k0, material): | ||
| kvec = Material(material).ks(k0) * qvec[:, None] | ||
| else: | ||
| kvec = qvec | ||
| poltype = config.POLTYPE if poltype is None else poltype | ||
| if poltype == "parity": | ||
| pol = [ | ||
| -sc.vpw_M(*kvec[:, 0], 0, 0, 0) @ pol, | ||
| sc.vpw_N(*kvec[:, 1], 0, 0, 0) @ pol, | ||
| ] | ||
| elif poltype == "helicity": | ||
| pol = sc.vpw_A(*kvec, 0, 0, 0, [1, 0]) @ pol | ||
| else: | ||
| raise ValueError(f"invalid 'poltype': {poltype}") | ||
| res = [pol[x[3]] * (np.abs(qvec - x[:3]) < 1e-14).all() for x in basis] | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) | ||
| def plane_wave( | ||
| kvec, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| """Array describing a plane wave. | ||
| Args: | ||
| kvec (Sequence): Wave vector. | ||
| pol (int or Sequence): Polarization index (see | ||
| :ref:`params:Polarizations`) to have a unit amplitude wave of the | ||
| corresponding wave, two values to specify the amplitude for each | ||
| polarization, or three values in a sequence to specify the Cartesian | ||
| electric field components. In the latter case, if the electric field has | ||
| longitudinal components they are neglected. | ||
| basis (PlaneWaveBasis, optional): Basis definition. | ||
| k0 (float, optional): Wave number in vacuum. | ||
| material (Material, optional): Material definition. | ||
| modetype (str, optional): Mode type (see :ref:`params:Mode types`). | ||
| poltype (str, optional): Polarization type (see | ||
| :ref:`params:Polarizations`). | ||
| """ | ||
| if len(kvec) == 2: | ||
| return _plane_wave_partial( | ||
| kvec, | ||
| pol, | ||
| k0=k0, | ||
| basis=basis, | ||
| material=material, | ||
| modetype=modetype, | ||
| poltype=poltype, | ||
| ) | ||
| if len(kvec) == 3: | ||
| return _plane_wave( | ||
| kvec, | ||
| pol, | ||
| k0=k0, | ||
| basis=basis, | ||
| material=material, | ||
| modetype=modetype, | ||
| poltype=poltype, | ||
| ) | ||
| raise ValueError(f"invalid length of 'kvec': {len(kvec)}") | ||
| def plane_wave_angle(theta, phi, pol, **kwargs): | ||
| qvec = [np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta)] | ||
| return plane_wave(qvec, pol, **kwargs) | ||
| def spherical_wave( | ||
| l, # noqa: E741 | ||
| m, | ||
| pol, | ||
| *, | ||
| k0=None, | ||
| basis=None, | ||
| material=None, | ||
| modetype=None, | ||
| poltype=None, | ||
| ): | ||
| if basis is None: | ||
| basis = SWB.default(l) | ||
| if not basis.isglobal: | ||
| raise ValueError("basis must be global") | ||
| res = [0] * len(basis) | ||
| res[basis.index((0, l, m, pol))] = 1 | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) | ||
| def cylindrical_wave( | ||
| kz, m, pol, *, k0=None, basis=None, material=None, modetype=None, poltype=None | ||
| ): | ||
| if basis is None: | ||
| basis = CWB.default([kz], abs(m)) | ||
| if not basis.isglobal: | ||
| raise ValueError("basis must be global") | ||
| res = [0] * len(basis) | ||
| res[basis.index((0, kz, m, pol))] = 1 | ||
| return PhysicsArray( | ||
| res, basis=basis, k0=k0, material=material, modetype=modetype, poltype=poltype | ||
| ) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import itertools | ||
| import numpy as np | ||
| import scipy.integrate | ||
| import treams.special as sc | ||
| def _j_real(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.real( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_A(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def _j_imag(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.imag( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_A(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def _rj_real(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.real( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_rA(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def _rj_imag(l0, l1, m, r, dr, pol0, pol1, ks, k, zs, z): | ||
| def fun(theta): | ||
| return np.imag( | ||
| np.sin(theta) | ||
| * ((2 * pol0 - 1) * z + (2 * pol1 - 1) * zs) | ||
| * np.dot( | ||
| [r(theta), -dr(theta), 0], | ||
| np.cross( | ||
| sc.vsw_rA(l0, -m, ks[pol0] * r(theta), theta, 0, pol0), | ||
| sc.vsw_rA(l1, m, k[pol1] * r(theta), theta, 0, pol1), | ||
| ), | ||
| ) | ||
| ) | ||
| return fun | ||
| def qmat(r, dr, ks, zs, out, in_=None, singular=True): | ||
| l_out, m_out, pol_out = out | ||
| in_ = out if in_ is None else in_ | ||
| l_in, m_in, pol_in = in_ | ||
| if singular: | ||
| fr = _j_real | ||
| fi = _j_imag | ||
| else: | ||
| fr = _rj_real | ||
| fi = _rj_imag | ||
| size_out = len(l_out) | ||
| size_in = len(l_in) | ||
| res = np.zeros((size_out, size_in), complex) | ||
| for i, j in itertools.product(range(size_out), range(size_in)): | ||
| if m_out[i] != m_in[j]: | ||
| continue | ||
| res[i, j] = ( | ||
| scipy.integrate.quad( | ||
| fr(l_out[i], l_in[j], m_out[i], r, dr, pol_out[i], pol_in[j], *ks, *zs), | ||
| 0, | ||
| np.pi, | ||
| )[0] | ||
| + 1j | ||
| * scipy.integrate.quad( | ||
| fi(l_out[i], l_in[j], m_out[i], r, dr, pol_out[i], pol_in[j], *ks, *zs), | ||
| 0, | ||
| np.pi, | ||
| )[0] | ||
| ) | ||
| return res |
-471
| """Loading and storing data. | ||
| Most functions rely on at least one of the external packages `h5py` or `gmsh`. | ||
| """ | ||
| import sys | ||
| import uuid as _uuid | ||
| from importlib.metadata import version | ||
| import numpy as np | ||
| import treams | ||
| try: | ||
| import h5py | ||
| except ImportError: | ||
| h5py = None | ||
| LENGTHS = { | ||
| "ym": 1e-24, | ||
| "zm": 1e-21, | ||
| "am": 1e-18, | ||
| "fm": 1e-15, | ||
| "pm": 1e-12, | ||
| "nm": 1e-9, | ||
| "um": 1e-6, | ||
| "µm": 1e-6, | ||
| "mm": 1e-3, | ||
| "cm": 1e-2, | ||
| "dm": 1e-1, | ||
| "m": 1, | ||
| "dam": 1e1, | ||
| "hm": 1e2, | ||
| "km": 1e3, | ||
| "Mm": 1e6, | ||
| "Gm": 1e9, | ||
| "Tm": 1e12, | ||
| "Pm": 1e15, | ||
| "Em": 1e18, | ||
| "Zm": 1e21, | ||
| "Ym": 1e24, | ||
| } | ||
| INVLENGTHS = { | ||
| r"ym^{-1}": 1e24, | ||
| r"zm^{-1}": 1e21, | ||
| r"am^{-1}": 1e18, | ||
| r"fm^{-1}": 1e15, | ||
| r"pm^{-1}": 1e12, | ||
| r"nm^{-1}": 1e9, | ||
| r"um^{-1}": 1e6, | ||
| r"µm^{-1}": 1e6, | ||
| r"mm^{-1}": 1e3, | ||
| r"cm^{-1}": 1e2, | ||
| r"dm^{-1}": 1e1, | ||
| r"m^{-1}": 1, | ||
| r"dam^{-1}": 1e-1, | ||
| r"hm^{-1}": 1e-2, | ||
| r"km^{-1}": 1e-3, | ||
| r"Mm^{-1}": 1e-6, | ||
| r"Gm^{-1}": 1e-9, | ||
| r"Tm^{-1}": 1e-12, | ||
| r"Pm^{-1}": 1e-15, | ||
| r"Em^{-1}": 1e-18, | ||
| r"Zm^{-1}": 1e-21, | ||
| r"Ym^{-1}": 1e-24, | ||
| } | ||
| FREQUENCIES = { | ||
| "yHz": 1e-24, | ||
| "zHz": 1e-21, | ||
| "aHz": 1e-18, | ||
| "fHz": 1e-15, | ||
| "pHz": 1e-12, | ||
| "nHz": 1e-9, | ||
| "uHz": 1e-6, | ||
| "µHz": 1e-6, | ||
| "mHz": 1e-3, | ||
| "cHz": 1e-2, | ||
| "dHz": 1e-1, | ||
| "s": 1, | ||
| "daHz": 1e1, | ||
| "hHz": 1e2, | ||
| "kHz": 1e3, | ||
| "MHz": 1e6, | ||
| "GHz": 1e9, | ||
| "THz": 1e12, | ||
| "PHz": 1e15, | ||
| "EHz": 1e18, | ||
| "ZHz": 1e21, | ||
| "YHz": 1e24, | ||
| r"ys^{-1}": 1e24, | ||
| r"zs^{-1}": 1e21, | ||
| r"as^{-1}": 1e18, | ||
| r"fs^{-1}": 1e15, | ||
| r"ps^{-1}": 1e12, | ||
| r"ns^{-1}": 1e9, | ||
| r"us^{-1}": 1e6, | ||
| r"µs^{-1}": 1e6, | ||
| r"ms^{-1}": 1e3, | ||
| r"cs^{-1}": 1e2, | ||
| r"ds^{-1}": 1e1, | ||
| r"s^{-1}": 1, | ||
| r"das^{-1}": 1e-1, | ||
| r"hs^{-1}": 1e-2, | ||
| r"ks^{-1}": 1e-3, | ||
| r"Ms^{-1}": 1e-6, | ||
| r"Gs^{-1}": 1e-9, | ||
| r"Ts^{-1}": 1e-12, | ||
| r"Ps^{-1}": 1e-15, | ||
| r"Es^{-1}": 1e-18, | ||
| r"Zs^{-1}": 1e-21, | ||
| r"Ys^{-1}": 1e-24, | ||
| } | ||
| def mesh_spheres(radii, positions, model, meshsize=None, meshsize_boundary=None): | ||
| """Generate a mesh of multiple spheres. | ||
| This function facilitates generating a mesh for a cluster spheres using gmsh. It | ||
| requires the package `gmsh` to be installed. | ||
| Examples: | ||
| >>> import gmsh | ||
| >>> import treams.io | ||
| >>> gmsh.initialize() | ||
| >>> gmsh.model.add("spheres") | ||
| >>> treams.io.mesh_spheres([1, 2], [[0, 0, 2], [0, 0, -2]], gmsh.model) | ||
| <class 'gmsh.model'> | ||
| >>> gmsh.write("spheres.msh") # doctest: +SKIP | ||
| >>> gmsh.finalize() # doctest: +SKIP | ||
| Args: | ||
| radii (float, array_like): Radii of the spheres. | ||
| positions (float, (N, 3)-array): Positions of the spheres. | ||
| model (gmsh.model): Gmsh model to modify. | ||
| meshsize (float, optional): Mesh size, if None a fifth of the largest radius is | ||
| used. | ||
| meshsize (float, optional): Mesh size of the surfaces, if left empty it is set | ||
| equal to the general mesh size. | ||
| Returns: | ||
| gmsh.model | ||
| """ | ||
| if meshsize is None: | ||
| meshsize = np.max(radii) * 0.2 | ||
| if meshsize_boundary is None: | ||
| meshsize_boundary = meshsize | ||
| spheres = [] | ||
| for i, (radius, position) in enumerate(zip(radii, positions)): | ||
| tag = i + 1 | ||
| model.occ.addSphere(*position, radius, tag) | ||
| spheres.append((3, tag)) | ||
| model.occ.synchronize() | ||
| for _, tag in spheres: | ||
| model.addPhysicalGroup(3, [tag], tag) | ||
| # Add surfaces for other mesh formats like stl, ... | ||
| model.addPhysicalGroup(2, [tag], tag) | ||
| model.mesh.setSize(model.getEntities(0), meshsize) | ||
| model.mesh.setSize( | ||
| model.getBoundary(spheres, False, False, True), meshsize_boundary | ||
| ) | ||
| return model | ||
| def _translate_polarizations(pols, poltype=None): | ||
| """Translate the polarization index into words. | ||
| The indices 0 and 1 are translated to "negative" and "positive", respectively, when | ||
| helicity modes are chosen. For parity modes they are translated to "magnetic" and | ||
| "electric". | ||
| Args: | ||
| pols (int, array_like): Array of indices 0 and 1. | ||
| poltype (str, optional): Polarization type (:ref:`polarizations.Polarizations`). | ||
| Returns: | ||
| list[str] | ||
| """ | ||
| poltype = treams.config.POLTYPE if poltype is None else poltype | ||
| if poltype == "helicity": | ||
| names = ["negative", "positive"] | ||
| elif poltype == "parity": | ||
| names = ["magnetic", "electric"] | ||
| else: | ||
| raise ValueError("unrecognized poltype") | ||
| return [names[i] for i in pols] | ||
| def _translate_polarizations_inv(pols): | ||
| """Translate the polarization into indices. | ||
| This function is the inverse of :func:`treams.io._translate_polarizations`. The | ||
| words "negative" and "minus" are translated to 0 and the words "positive" and "plus" | ||
| are translated to 1, if helicity modes are chosen. For parity modes, modes | ||
| "magnetic" or "te" are translated to 0 and the modes "electric" or "tm" to 1. | ||
| Args: | ||
| pols (string, array_like): Array of strings | ||
| Returns: | ||
| tuple[list[int], str] | ||
| """ | ||
| helicity = {"plus": 1, "positive": 1, "minus": 0, "negative": 0} | ||
| parity = {"te": 0, "magnetic": 0, "tm": 1, "electric": 1, "M": 0, "N": 1} | ||
| if pols[0].decode() in helicity: | ||
| dct = helicity | ||
| poltype = "helicity" | ||
| elif pols[0].decode() in parity: | ||
| dct = parity | ||
| poltype = "parity" | ||
| else: | ||
| raise ValueError(f"unrecognized polarization '{pols[0].decode()}'") | ||
| return [dct[i.decode()] for i in pols], poltype | ||
| def _remove_leading_ones(shape): | ||
| while len(shape) > 0 and shape[0] == 1: | ||
| shape = shape[1:] | ||
| return shape | ||
| def _collapse_dims(arr): | ||
| for i in range(arr.ndim): | ||
| test = arr[(slice(None),) * i + (slice(1),)] | ||
| if np.all(arr == test): | ||
| arr = test | ||
| return arr.reshape(_remove_leading_ones(arr.shape)) | ||
| def save_hdf5( | ||
| h5file, | ||
| tms, | ||
| name="", | ||
| description="", | ||
| keywords="", | ||
| embedding_group=None, | ||
| embedding_name="", | ||
| embedding_description="", | ||
| embedding_keywords="", | ||
| uuid=None, | ||
| uuid_version=4, | ||
| lunit="nm", | ||
| ): | ||
| """Save a set of T-matrices in a HDF5 file. | ||
| With an open and writeable datafile, this function stores the main parts of as | ||
| T-matrix in the file. It is left open for the user to add additional metadata. | ||
| Args: | ||
| h5file (h5py.Group): A HDF5 file opened with h5py. | ||
| tms (TMatrix, array_like): Array of T-matrix instances. | ||
| name (str): Name to add to the file as attribute. | ||
| description (str): Description to add to file as attribute. | ||
| keywords (str): Keywords to add to file as attribute. | ||
| embedding_group (h5py.Group, optional): Group object for the embedding material, | ||
| defaults to "/materials/embedding/". | ||
| embedding_name (string, optional): Name of the embedding material. | ||
| embedding_description (string, optional): Description of the embedding material. | ||
| embedding_keywords (string, optional): Keywords for the embedding material. | ||
| uuid (bytes, optional): UUID of the file, a new one is created if omitted. | ||
| uuid_version (int, optional): UUID version. | ||
| lunit (string, optional): Length unit used for the positions and (as | ||
| inverse) for the wave number. | ||
| """ | ||
| tms_arr = np.array(tms) | ||
| if tms_arr.dtype == object: | ||
| raise ValueError("can only save T-matrices of the same size") | ||
| tms_obj = np.empty(tms_arr.shape[:-2], object) | ||
| tms_obj[:] = tms | ||
| tm = tms_obj.flat[0] | ||
| basis = tm.basis | ||
| poltype = tm.poltype | ||
| k0s = np.zeros((tms_arr.shape)[:-2]) | ||
| epsilon = np.zeros((tms_arr.shape)[:-2], complex) | ||
| mu = np.zeros((tms_arr.shape)[:-2], complex) | ||
| if poltype == "helicity": | ||
| kappa = np.zeros((tms_arr.shape)[:-2], complex) | ||
| for i, tm in enumerate(tms_obj.flat): | ||
| if poltype != tm.poltype or basis != tm.basis: | ||
| raise ValueError( | ||
| "incompatible T-matrices: mixed poltypes or different bases" | ||
| ) | ||
| k0s.flat[i] = tm.k0 | ||
| epsilon.flat[i] = tm.material.epsilon | ||
| mu.flat[i] = tm.material.mu | ||
| if poltype == "helicity": | ||
| kappa.flat[i] = tm.material.kappa | ||
| h5file["tmatrix"] = tms_arr | ||
| if uuid is None: | ||
| h5file["uuid"] = np.void(_uuid.uuid4().bytes) | ||
| h5file["uuid"].attrs["version"] = 4 | ||
| else: | ||
| h5file["uuid"] = uuid | ||
| h5file["uuid"].attrs["version"] = uuid_version | ||
| _name_descr_kw(h5file, name, description, keywords) | ||
| h5file["angular_vacuum_wavenumber"] = _collapse_dims(k0s) | ||
| h5file["angular_vacuum_wavenumber"].attrs["unit"] = lunit + r"^{-1}" | ||
| h5file["modes/l"] = basis.l | ||
| h5file["modes/m"] = basis.m | ||
| h5file["modes/polarization"] = _translate_polarizations(basis.pol, poltype) | ||
| if any(basis.pidx != 0): | ||
| h5file["modes/pidx"] = basis.pidx | ||
| if not np.array_equiv(basis.positions, [[0, 0, 0]]): | ||
| h5file["modes/positions"] = basis.positions | ||
| h5file["modes/positions"].attrs["unit"] = lunit | ||
| if embedding_group is None: | ||
| embedding_group = h5file.create_group("materials/embedding") | ||
| embedding_group["relative_permittivity"] = _collapse_dims(epsilon) | ||
| embedding_group["relative_permeability"] = _collapse_dims(mu) | ||
| if poltype == "helicity": | ||
| embedding_group["chirality"] = _collapse_dims(kappa) | ||
| _name_descr_kw( | ||
| embedding_group, embedding_name, embedding_description, embedding_keywords | ||
| ) | ||
| h5file["embedding"] = h5py.SoftLink(embedding_group.name) | ||
| h5file.attrs["created_with"] = ( | ||
| f"python={sys.version.split()[0]}," | ||
| f"h5py={version('h5py')}," | ||
| f"treams={version('treams')}" | ||
| ) | ||
| h5file.attrs["storage_format_version"] = "0.0.1-4-g1266244" | ||
| def _name_descr_kw(fobj, name, description="", keywords=""): | ||
| for key, val in [ | ||
| ("name", name), | ||
| ("description", description), | ||
| ("keywords", keywords), | ||
| ]: | ||
| val = str(val) | ||
| if val != "": | ||
| fobj.attrs[key] = val | ||
| def _convert_to_k0(x, xtype, xunit, k0unit=r"nm^{-1}"): | ||
| c = 299792458.0 | ||
| k0unit = INVLENGTHS[k0unit] | ||
| if xtype == "frequency": | ||
| xunit = FREQUENCIES[xunit] | ||
| return 2 * np.pi * x / c * (xunit / k0unit) | ||
| if xtype == "angular_frequency": | ||
| xunit = FREQUENCIES[xunit] | ||
| return x / c * (xunit / k0unit) | ||
| if xtype == "vacuum_wavelength": | ||
| xunit = LENGTHS[xunit] | ||
| return 2 * np.pi / (x * xunit * k0unit) | ||
| if xtype == "vacuum_wavenumber": | ||
| xunit = INVLENGTHS[xunit] | ||
| return 2 * np.pi * x * (xunit / k0unit) | ||
| if xtype == "angular_vacuum_wavenumber": | ||
| xunit = INVLENGTHS[xunit] | ||
| return x * (xunit / k0unit) | ||
| raise ValueError(f"unrecognized frequency/wavenumber/wavelength type: {xtype}") | ||
| def load_hdf5(filename, lunit="nm"): | ||
| """Load a T-matrix stored in a HDF4 file. | ||
| Args: | ||
| filename (str or h5py.Group): Name of the h5py file or a handle to a h5py group. | ||
| lunit (str, optional): Unit of length to be used in the T-matrices. | ||
| Returns: | ||
| np.ndarray[TMatrix] | ||
| """ | ||
| if isinstance(filename, h5py.Group): | ||
| return _load_hdf5(filename, lunit) | ||
| with h5py.File(filename, "r") as f: | ||
| return _load_hdf5(f, lunit) | ||
| def _load_hdf5(h5file, lunit=None): | ||
| for freq_type in ( | ||
| "frequency", | ||
| "angular_frequency", | ||
| "vacuum_wavelength", | ||
| "vacuum_wavenumber", | ||
| "angular_vacuum_wavenumber", | ||
| ): | ||
| if freq_type in h5file: | ||
| ld_freq = h5file[freq_type][()] | ||
| break | ||
| else: | ||
| raise ValueError("no definition of frequency found") | ||
| if "modes/positions" in h5file: | ||
| k0unit = h5file["modes/positions"].attrs.get("unit", lunit) + r"^{-1}" | ||
| else: | ||
| k0unit = lunit + r"^{-1}" | ||
| tms = h5file["tmatrix"][...] | ||
| k0s = _convert_to_k0(ld_freq, freq_type, h5file[freq_type].attrs["unit"], k0unit) | ||
| epsilon = h5file.get("embedding/relative_permittivity", np.array(None))[()] | ||
| mu = h5file.get("embedding/relative_permeability", np.array(None))[()] | ||
| if epsilon is None is mu: | ||
| n = h5file.get("embedding/refractive_index", np.array(1))[...] | ||
| z = h5file.get("embedding/relative_impedance", 1 / n)[...] | ||
| epsilon = n / z | ||
| mu = n * z | ||
| epsilon = 1 if epsilon is None else epsilon | ||
| mu = 1 if mu is None else mu | ||
| kappa = z = h5file.get("embedding/chirality_parameter", np.array(0))[...] | ||
| positions = h5file.get("modes/positions", np.zeros((1, 3)))[...] | ||
| l_inc = h5file.get("modes/l", np.array(None))[...] | ||
| l_inc = h5file.get("modes/l_incident", l_inc)[()] | ||
| m_inc = h5file.get("modes/m", np.array(None))[...] | ||
| m_inc = h5file.get("modes/m_incident", m_inc)[()] | ||
| pol_inc = h5file.get("modes/polarization", np.array(None))[...] | ||
| pol_inc = h5file.get("modes/polarization_incident", pol_inc)[()] | ||
| l_sca = h5file.get("modes/l", np.array(None))[...] | ||
| l_sca = h5file.get("modes/l_scattered", l_sca)[()] | ||
| m_sca = h5file.get("modes/m", np.array(None))[...] | ||
| m_sca = h5file.get("modes/m_scattered", m_sca)[()] | ||
| pol_sca = h5file.get("modes/polarization", np.array(None))[...] | ||
| pol_sca = h5file.get("modes/polarization_scattered", pol_sca)[()] | ||
| if any(x is None for x in (l_inc, l_sca, m_inc, m_sca, pol_inc, pol_sca)): | ||
| raise ValueError("mode definition missing") | ||
| pol_inc, poltype = _translate_polarizations_inv(pol_inc) | ||
| pol_sca, poltype_sca = _translate_polarizations_inv(pol_sca) | ||
| if poltype_sca != poltype: | ||
| raise ValueError("different modetypes") | ||
| pidx_inc = h5file.get("modes/position_index", np.zeros_like(l_inc))[()] | ||
| pidx_inc = h5file.get("modes/positions_index_scattered", pidx_inc)[()] | ||
| pidx_sca = h5file.get("modes/position_index", np.zeros_like(l_sca))[()] | ||
| pidx_sca = h5file.get("modes/positions_index_scattered", pidx_sca)[()] | ||
| shape = tms.shape[:-2] | ||
| k0s = np.broadcast_to(k0s, shape) | ||
| epsilon = np.broadcast_to(epsilon, shape) | ||
| mu = np.broadcast_to(mu, shape) | ||
| kappa = np.broadcast_to(kappa, shape) | ||
| basis_inc = treams.SphericalWaveBasis( | ||
| zip(pidx_inc, l_inc, m_inc, pol_inc), positions | ||
| ) | ||
| basis_sca = treams.SphericalWaveBasis( | ||
| zip(pidx_sca, l_sca, m_sca, pol_sca), positions | ||
| ) | ||
| basis = basis_inc | basis_sca | ||
| ix_inc = [basis.index(b) for b in basis_inc] | ||
| ix_sca = [[basis.index(b)] for b in basis_sca] | ||
| res = np.empty(shape, object) | ||
| for i in np.ndindex(*shape): | ||
| res[i] = treams.TMatrix( | ||
| np.zeros((len(basis),) * 2, complex), | ||
| k0=k0s[i], | ||
| basis=basis, | ||
| poltype=poltype, | ||
| material=treams.Material(epsilon[i], mu[i], kappa[i]), | ||
| ) | ||
| res[i][ix_sca, ix_inc] = tms[i] | ||
| if not res.shape: | ||
| res = res.item() | ||
| return res |
| r"""Lattice sums. | ||
| .. currentmodule:: treams.lattice | ||
| Calculates the lattice sums of the forms | ||
| .. math:: | ||
| D_{lm}(k, \boldsymbol k_\parallel, \boldsymbol r, \Lambda_d) | ||
| = \sideset{}{'}{\sum_{\boldsymbol R \in \Lambda_d}} | ||
| h_l^{(1)}(k|\boldsymbol r + \boldsymbol R|) | ||
| Y_{lm} | ||
| (\theta_{-\boldsymbol r - \boldsymbol R}, \varphi_{-\boldsymbol r - \boldsymbol R}) | ||
| \mathrm e^{\mathrm i \boldsymbol k_\parallel \boldsymbol R} | ||
| and | ||
| .. math:: | ||
| D_{m}(k, \boldsymbol k_\parallel, \boldsymbol r, \Lambda_d) | ||
| = \sideset{}{'}{\sum_{\boldsymbol R \in \Lambda_d}} | ||
| H_m^{(1)}(k|\boldsymbol r + \boldsymbol R|) | ||
| \mathrm e^{\mathrm i m \varphi_{-\boldsymbol r - \boldsymbol R}} | ||
| \mathrm e^{\mathrm i \boldsymbol k_\parallel \boldsymbol R} | ||
| that arise when translating and summing the spherical and cylindrical solutions | ||
| of the Helmholtz equation in a periodic arrangement. These sums have a notoriously slow | ||
| convergence and at least for lattices with a dimension :math:`d > 1` it is not advisable | ||
| to use the direct approach. Fortunately, it is possible to convert them to exponentially | ||
| convergent series, which are implemented here. For details on the math, see the | ||
| references below. | ||
| The lattice of dimension :math:`d \leq 3` (:math:`d \leq 2` in the second case) is | ||
| denoted with :math:`\Lambda_d` and consists of all vectors | ||
| :math:`\boldsymbol R = \sum_{i=1}^{d} n_i \boldsymbol a_i` with :math:`\boldsymbol a_i` | ||
| being basis vectors of the lattice and :math:`n_i \in \mathbb Z`. For :math:`d = 2` the | ||
| lattice is in the `z = 0` plane and for :math:`d = 1` it is along the z-axis. The vector | ||
| :math:`\boldsymbol r` is arbitrary but for best convergence it should be reduced to the | ||
| Wigner-Seitz cell of the lattice. The summation excludes the point | ||
| :math:`\boldsymbol R + \boldsymbol r = 0` if it exists, which is indicated by the prime | ||
| next to the summation sign. | ||
| The wave in the lattice is defined by its -- possibly complex-valued -- wave number | ||
| :math:`k` and the (real) components of the wave vector | ||
| :math:`\boldsymbol k_\parallel \in \mathbb R^d` that are parallel to the lattice. | ||
| The expressions include the (spherical) Hankel functions of first kind :math:`H_m^{(1)}` | ||
| (:math:`h_l^{(1)}`) and the spherical harmonics :math:`Y_{lm}`. The angles | ||
| :math:`\theta` and :math:`\varphi` are the polar and azimuthal angle, when expressing | ||
| the points in spherical coordinates. In the first case, the degree is | ||
| :math:`l \in \mathbb N_0` and the order is :math:`\mathbb Z \ni m \leq l`. In the second | ||
| case :math:`m \in \mathbb Z`. | ||
| Available functions | ||
| =================== | ||
| Accelerated lattice summations | ||
| ------------------------------ | ||
| .. autosummary:: | ||
| :toctree: | ||
| lsumcw1d | ||
| lsumcw1d_shift | ||
| lsumcw2d | ||
| lsumsw1d | ||
| lsumsw1d_shift | ||
| lsumsw2d | ||
| lsumsw2d_shift | ||
| lsumsw3d | ||
| Direct summations | ||
| ----------------- | ||
| The functions are almost only here for benchmarking and comparison. | ||
| .. autosummary:: | ||
| :toctree: | ||
| dsumcw1d | ||
| dsumcw1d_shift | ||
| dsumcw2d | ||
| dsumsw1d | ||
| dsumsw1d_shift | ||
| dsumsw2d | ||
| dsumsw2d_shift | ||
| dsumsw3d | ||
| Miscellaneous functions | ||
| ----------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| area | ||
| cube | ||
| cubeedge | ||
| diffr_orders_circle | ||
| reciprocal | ||
| volume | ||
| Cython module | ||
| ------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| :template: cython-module | ||
| cython_lattice | ||
| References | ||
| ========== | ||
| * `[1] K. Kambe, Zeitschrift Für Naturforschung A 22, 3 (1967). <https://doi.org/10.1515/zna-1967-0305>`_ | ||
| * `[2] K. Kambe, Zeitschrift Für Naturforschung A 23, 9 (1968). <https://doi.org/10.1515/zna-1968-0908>`_ | ||
| * `[3] C. M. Linton, SIAM Rev. 52, 630 (2010). <https://doi.org/10.1137/09075130X>`_ | ||
| * `[4] D. Beutel el al., J. Opt. Soc. Am. B (2021). <https://doi.org/10.1364/JOSAB.419645>`_ | ||
| """ | ||
| import numpy as np | ||
| from treams.lattice import _misc | ||
| from treams.lattice._gufuncs import * # noqa: F403 | ||
| from treams.lattice._misc import cube, cubeedge # noqa: F401 | ||
| def diffr_orders_circle(b, rmax): | ||
| """Diffraction orders in a circle. | ||
| Given the reciprocal lattice defined by the vectors that make up the rows of `b`, | ||
| return all diffraction orders within a circle of radius `rmax`. | ||
| Args: | ||
| b (float, (2, 2)-array): Reciprocal lattice vectors | ||
| rmax (float): Maximal radius | ||
| Returns: | ||
| float array | ||
| """ | ||
| return _misc.diffr_orders_circle(np.array(b), rmax) | ||
| def lsumsw(dim, l, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return lsumsw1d_shift(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return lsumsw2d_shift(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 3: | ||
| return lsumsw3d(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def realsumsw(dim, l, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return realsumsw1d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 2: | ||
| return realsumsw2d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 3: | ||
| return realsumsw3d(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def recsumsw(dim, l, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return recsumsw1d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 2: | ||
| return recsumsw2d_shift( # noqa: F405 | ||
| l, m, k, kpar, a, r, eta, out=out, **kwargs | ||
| ) | ||
| if dim == 3: | ||
| return recsumsw3d(l, m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def lsumcw(dim, m, k, kpar, a, r, eta, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return lsumcw1d_shift(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return lsumcw2d(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def realsumcw(dim, m, k, kpar, a, r, eta, out=None, **kwargs): | ||
| if dim == 1: | ||
| return realsumcw1d_shift(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return realsumcw2d(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def recsumcw(dim, m, k, kpar, a, r, eta, out=None, **kwargs): | ||
| if dim == 1: | ||
| return recsumcw1d_shift(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return recsumcw2d(m, k, kpar, a, r, eta, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def dsumsw(dim, l, m, k, kpar, a, r, i, out=None, **kwargs): # noqa: E741 | ||
| if dim == 1: | ||
| return dsumsw1d_shift(l, m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return dsumsw2d_shift(l, m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| if dim == 3: | ||
| return dsumsw3d(l, m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") | ||
| def dsumcw(dim, m, k, kpar, a, r, i, out=None, **kwargs): | ||
| if dim == 1: | ||
| return dsumcw1d_shift(m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| if dim == 2: | ||
| return dsumcw2d(m, k, kpar, a, r, i, out=out, **kwargs) # noqa: F405 | ||
| raise ValueError(f"invalid dimension '{dim}'") |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| ctypedef fused real_t: | ||
| double | ||
| long | ||
| ctypedef fused number_t: | ||
| double complex | ||
| double | ||
| cdef real_t area(real_t *a, real_t *b) nogil | ||
| cdef long cube_next(long *r, long d, long n) nogil | ||
| cdef long cubeedge_next(long *r, long d, long n) nogil | ||
| cpdef diffr_orders_circle(real_t[:, :] b, double rmax) | ||
| cpdef long ncube(long d, long n) nogil | ||
| cpdef long nedge(long d, long n) nogil | ||
| cdef void recvec2(double *a0, double *a1, double *b0, double *b1) nogil | ||
| cdef void recvec3(double *a0, double *a1, double *a2, double *b0, double *b1, double *b2) nogil | ||
| cdef real_t volume(real_t *a, real_t *b, real_t *c) nogil | ||
| cpdef double complex lsumcw1d(long l, number_t k, double kpar, double a, double r, number_t eta) nogil | ||
| cdef double complex lsumcw1d_shift(long l, number_t k, double kpar, double a, double *r, number_t eta) nogil | ||
| cdef double complex lsumcw2d(long l, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cpdef double complex lsumsw1d(long l, number_t k, double kpar, double a, double r, number_t eta) nogil | ||
| cdef double complex lsumsw1d_shift(long l, long m, number_t k, double kpar, double a, double *r, number_t eta) nogil | ||
| cdef double complex lsumsw2d(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cdef double complex lsumsw2d_shift(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cdef double complex lsumsw3d(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cpdef double complex realsumcw1d(long l, number_t k, double kpar, double a, double r, number_t eta) nogil | ||
| cdef double complex realsumcw1d_shift(long l, number_t k, double kpar, double a, double *r, number_t eta) nogil | ||
| cdef double complex realsumcw2d(long l, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cpdef double complex realsumsw1d(long l, number_t k, double kpar, double a, double r, number_t eta) nogil | ||
| cdef double complex realsumsw1d_shift(long l, long m, number_t k, double kpar, double a, double *r, number_t eta) nogil | ||
| cdef double complex realsumsw2d(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cdef double complex realsumsw2d_shift(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cdef double complex realsumsw3d(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cpdef double complex recsumcw1d(long l, number_t k, double kpar, double a, double r, number_t eta) nogil | ||
| cdef double complex recsumcw1d_shift(long l, number_t k, double kpar, double a, double *r, number_t eta) nogil | ||
| cdef double complex recsumcw2d(long l, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cpdef double complex recsumsw1d(long l, number_t k, double kpar, double a, double r, number_t eta) nogil | ||
| cdef double complex recsumsw1d_shift(long l, long m, number_t k, double kpar, double a, double *r, number_t eta) nogil | ||
| cdef double complex recsumsw2d(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cdef double complex recsumsw2d_shift(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cdef double complex recsumsw3d(long l, long m, number_t k, double *kpar, double *a, double *r, number_t eta) nogil | ||
| cpdef double complex zero3d(double complex eta) nogil | ||
| cpdef double complex zero2d(double complex eta) nogil | ||
| cpdef double complex dsumcw1d(long l, number_t k, double kpar, double a, double r, long i) nogil | ||
| cdef double complex dsumcw1d_shift(long l, number_t k, double kpar, double a, double *r, long i) nogil | ||
| cdef double complex dsumcw2d(long l, number_t k, double *kpar, double *a, double *r, long i) nogil | ||
| cpdef double complex dsumsw1d(long l, number_t k, double kpar, double a, double r, long i) nogil | ||
| cdef double complex dsumsw1d_shift(long l, long m, number_t k, double kpar, double a, double *r, long i) nogil | ||
| cdef double complex dsumsw2d(long l, long m, number_t k, double *kpar, double *a, double *r, long i) nogil | ||
| cdef double complex dsumsw2d_shift(long l, long m, number_t k, double *kpar, double *a, double *r, long i) nogil | ||
| cdef double complex dsumsw3d(long l, long m, number_t k, double *kpar, double *a, double *r, long i) nogil |
| """Miscellaneous functions. | ||
| .. autosummary:: | ||
| :toctree: | ||
| basischange | ||
| firstbrillouin1d | ||
| firstbrillouin2d | ||
| firstbrillouin3d | ||
| pickmodes | ||
| refractive_index | ||
| wave_vec_z | ||
| """ | ||
| import numpy as np | ||
| import treams.lattice as la | ||
| def refractive_index(epsilon=1, mu=1, kappa=0): | ||
| r"""Refractive index of a (chiral) medium. | ||
| The refractive indeces in a chiral medium :math:`\sqrt{\epsilon\mu} \mp \kappa` are | ||
| returned with the negative helicity result first. | ||
| Args: | ||
| epsilon (float or complex, array_like, optional): Relative permittivity, | ||
| defaults to 1. | ||
| mu (float or complex, array_like, optional): Relative permeability, | ||
| defaults to 1. | ||
| kappa (float or complex, array_like, optional): Chirality parameter, | ||
| defaults to 0. | ||
| Returns: | ||
| float or complex, (2,)-array | ||
| """ | ||
| epsilon = np.array(epsilon) | ||
| n = np.sqrt(epsilon * mu) | ||
| res = np.stack((n - kappa, n + kappa), axis=-1) | ||
| res[np.imag(res) < 0] *= -1 | ||
| return res | ||
| def basischange(out, in_=None): | ||
| """Coefficients for the basis change between helicity and parity modes. | ||
| Args: | ||
| out (3- or 4-tuple of (M,)-arrays): Output modes, the last array is taken as | ||
| polarization. | ||
| in_ (3- or 4-tuple of (N,)-arrays, optional): Input modes, if none are given, | ||
| equal to the output modes | ||
| Returns: | ||
| float, ((M, N)-array | ||
| """ | ||
| if in_ is None: | ||
| in_ = out | ||
| out = np.array([*zip(*out)]) | ||
| in_ = np.array([*zip(*in_)]) | ||
| res = np.zeros((out.shape[0], in_.shape[0])) | ||
| out = out[:, None, :] | ||
| sqhalf = np.sqrt(0.5) | ||
| equal = (out[:, :, :-1] == in_[:, :-1]).all(axis=-1) | ||
| minus = np.logical_and(out[:, :, -1] == in_[:, -1], in_[:, -1] == 0) | ||
| res[equal] = sqhalf | ||
| res[np.logical_and(equal, minus)] = -sqhalf | ||
| return res | ||
| def pickmodes(out, in_): | ||
| """Coefficients to pick modes. | ||
| Args: | ||
| out (3- or 4-tuple of (M,)-arrays): Output modes, the last array is taken as | ||
| polarization. | ||
| in_ (3- or 4-tuple of (N,)-arrays, optional): Input modes, if none are given, | ||
| equal to the output modes | ||
| Returns: | ||
| float, ((M, N)-array | ||
| """ | ||
| out = np.array([*zip(*out)]) | ||
| in_ = np.array([*zip(*in_)]) | ||
| return np.all(out[:, None, :] == in_, axis=-1) | ||
| # def wave_vec_zs(kpars, ks): | ||
| # kpars = np.array(kpars) | ||
| # ks = np.array(ks) | ||
| # if ks.ndim == 0: | ||
| # nmat = npol = 1 | ||
| # elif ks.ndim == 1: | ||
| # npol = ks.shape[0] | ||
| # nmat = 1 | ||
| # elif ks.ndim == 2: | ||
| # nmat, npol = ks.shape | ||
| # else: | ||
| # raise ValueError("ks has invalid shape") | ||
| # res = np.zeros((nmat, kpars.shape[0], npol), np.complex128) | ||
| # for i, kpar in enumerate(kpars): | ||
| # res[:, i, :] = wave_vec_z(kpar[0], kpar[1], ks) | ||
| # return res | ||
| def wave_vec_z(kx, ky, k): | ||
| r"""Z component of the wave vector with positive imaginary part. | ||
| The result is :math:`k_z = \sqrt{k^2 - k_x^2 - k_y^2}` with | ||
| :math:`\arg k_z \in \[ 0, \pi )`. | ||
| Args: | ||
| kx (float, array_like): X component of the wave vector | ||
| ky (float, array_like): Y component of the wave vector | ||
| k (float or complex, array_like): Wave number | ||
| Returns: | ||
| complex | ||
| """ | ||
| kx = np.asarray(kx) | ||
| ky = np.asarray(ky) | ||
| k = np.asarray(k, complex) | ||
| res = np.sqrt(k * k - kx * kx - ky * ky) | ||
| if res.ndim == 0 and res.imag < 0: | ||
| res = -res | ||
| elif res.ndim > 0: | ||
| res[np.imag(res) < 0] *= -1 | ||
| return res | ||
| def firstbrillouin1d(kpar, b): | ||
| """Map wave vector to first Brillouin zone in 1D. | ||
| Reduce the 1d wave vector (actually just a number) to the first Brillouin zone, i.e. | ||
| the range `(-b/2, b/2]` | ||
| Args: | ||
| kpar (float64): (parallel) wave vector | ||
| b (float64): reciprocal lattice vector | ||
| Returns: | ||
| float64 | ||
| """ | ||
| kpar -= b * np.round(kpar / b) | ||
| if kpar > 0.5 * b: | ||
| kpar -= b | ||
| if kpar <= -0.5 * b: | ||
| kpar += b | ||
| return kpar | ||
| def firstbrillouin2d(kpar, b, n=2): | ||
| """Map wave vector to first Brillouin zone in 2D. | ||
| The reduction to the first Brillouin zone is first approximated roughly. From this | ||
| approximated vector and its 8 neighbours, the shortest one is picked. As a | ||
| sufficient approximation is not guaranteed (especially for extreme geometries), | ||
| this process is iterated `n` times. | ||
| Args: | ||
| kpar (1d-array): parallel wave vector | ||
| b (2d-array): reciprocal lattice vectors | ||
| n (int): number of iterations | ||
| Returns: | ||
| (1d-array) | ||
| """ | ||
| kparstart = kpar | ||
| b1 = b[0, :] | ||
| b2 = b[1, :] | ||
| normsq1 = b1 @ b1 | ||
| normsq2 = b2 @ b2 | ||
| normsqp = (b1 + b2) @ (b1 + b2) | ||
| normsqm = (b1 - b2) @ (b1 - b2) | ||
| if ( | ||
| normsqp < normsq1 - 1e-14 | ||
| or normsqp < normsq2 - 1e-14 | ||
| or normsqm < normsq1 - 1e-14 | ||
| or normsqm < normsq2 - 1e-14 | ||
| ): | ||
| raise ValueError("Lattice vectors are not of minimal length") | ||
| # Rough estimate | ||
| kpar -= b1 * np.round((kpar @ b1) / normsq1) | ||
| kpar -= b2 * np.round((kpar @ b2) / normsq2) | ||
| # todo: precise | ||
| options = kpar + la.cube(2, 1) @ b | ||
| for option in options: | ||
| if option @ option < kpar @ kpar: | ||
| kpar = option | ||
| if n == 0 or np.array_equal(kpar, kparstart): | ||
| return kpar | ||
| return firstbrillouin2d(kpar, b, n - 1) | ||
| def firstbrillouin3d(kpar, b, n=2): | ||
| """Map wave vector to first Brillouin zone in 3D. | ||
| The reduction to the first Brillouin zone is first approximated roughly. From this | ||
| approximated vector and its 26 neighbours, the shortest one is picked. As a | ||
| sufficient approximation is not guaranteed (especially for extreme geometries), | ||
| this process is iterated `n` times. | ||
| Args: | ||
| kpar (1d-array): parallel wave vector | ||
| b (2d-array): reciprocal lattice vectors | ||
| n (int): number of iterations | ||
| Returns: | ||
| (1d-array) | ||
| """ | ||
| kparstart = kpar | ||
| b1 = b[0, :] | ||
| b2 = b[1, :] | ||
| b3 = b[2, :] | ||
| normsq1 = b1 @ b1 | ||
| normsq2 = b2 @ b2 | ||
| normsq3 = b3 @ b3 | ||
| # todo: Minimal length | ||
| # Rough estimate | ||
| kpar -= b1 * np.round((kpar @ b1) / normsq1) | ||
| kpar -= b2 * np.round((kpar @ b2) / normsq2) | ||
| kpar -= b3 * np.round((kpar @ b3) / normsq3) | ||
| # todo: precise | ||
| options = kpar + la.cube(3, 1) @ b | ||
| for option in options: | ||
| if option @ option < kpar @ kpar: | ||
| kpar = option | ||
| if n == 0 or np.array_equal(kpar, kparstart): | ||
| return kpar | ||
| return firstbrillouin3d(kpar, b, n - 1) |
Sorry, the diff of this file is too big to display
| r"""Special (mathematical) functions. | ||
| .. currentmodule:: treams.special | ||
| Special mathematical functions used in :mod:`treams`. Some functions are reexported from | ||
| :py:mod:`scipy.special`. Most functions are available as Numpy universal functions | ||
| (:py:class:`numpy.ufunc`) or as generalized universal functions | ||
| (:ref:`c-api.generalized-ufuncs`). | ||
| Available functions | ||
| =================== | ||
| Bessel and Hankel functions, with their spherical counterparts, derivatives | ||
| --------------------------------------------------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| hankel1_d | ||
| hankel2_d | ||
| jv_d | ||
| yv_d | ||
| spherical_hankel1 | ||
| spherical_hankel2 | ||
| spherical_hankel1_d | ||
| spherical_hankel2_d | ||
| Those functions are just reexported from Scipy. So, one only needs to import this | ||
| subpackage within treams. | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.hankel1`\(v, z[, out]) | Hankel function of the | | ||
| | | first kind. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.hankel2`\(v, z[, out]) | Hankel function of the | | ||
| | | second kind. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.jv`\(v, z[, out]) | Bessel function of the | | ||
| | | first kind of real | | ||
| | | order and complex | | ||
| | | argument. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | :py:data:`~scipy.special.yv`\(v, z[, out]) | Bessel function of the | | ||
| | | second kind of real | | ||
| | | order and complex | | ||
| | | argument. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | | :py:func:`spherical_jn <scipy.special.spherical_jn>`\(n, | Spherical Bessel | | ||
| | z[, derivative]) | function of the first | | ||
| | | kind or its derivative. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| | | :py:func:`spherical_yn <scipy.special.spherical_yn>`\(n, | Spherical Bessel | | ||
| | z[, derivative]) | function of the second | | ||
| | | kind or its derivative. | | ||
| +------------------------------------------------------------+-------------------------+ | ||
| Those functions just wrap Scipy functions with special optional arguments to be able to | ||
| analogously access them like their non-spherical counterparts: | ||
| .. autosummary:: | ||
| :toctree: | ||
| spherical_jn_d | ||
| spherical_yn_d | ||
| Scipy functions with enhanced domain | ||
| ------------------------------------ | ||
| .. autosummary:: | ||
| :toctree: | ||
| sph_harm | ||
| lpmv | ||
| Integrals for the Ewald summation | ||
| --------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| incgamma | ||
| intkambe | ||
| Wigner d- and Wigner D-matrix elements | ||
| -------------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| wignersmalld | ||
| wignerd | ||
| Wigner 3j-symbols | ||
| ----------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| wigner3j | ||
| Vector wave functions | ||
| -------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| pi_fun | ||
| tau_fun | ||
| Spherical waves and translation coefficients | ||
| .. autosummary:: | ||
| :toctree: | ||
| vsh_X | ||
| vsh_Y | ||
| vsh_Z | ||
| vsw_M | ||
| vsw_N | ||
| vsw_A | ||
| vsw_rM | ||
| vsw_rN | ||
| vsw_rA | ||
| tl_vsw_A | ||
| tl_vsw_B | ||
| tl_vsw_rA | ||
| tl_vsw_rB | ||
| Cylindrical waves | ||
| .. autosummary:: | ||
| :toctree: | ||
| vcw_M | ||
| vcw_N | ||
| vcw_A | ||
| vcw_rM | ||
| vcw_rN | ||
| vcw_rA | ||
| tl_vcw | ||
| tl_vcw_r | ||
| Plane waves | ||
| .. autosummary:: | ||
| :toctree: | ||
| vpw_M | ||
| vpw_N | ||
| vpw_A | ||
| Coordinate system transformations | ||
| --------------------------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| car2cyl | ||
| car2sph | ||
| cyl2car | ||
| cyl2sph | ||
| sph2car | ||
| sph2cyl | ||
| vcar2cyl | ||
| vcar2sph | ||
| vcyl2car | ||
| vcyl2sph | ||
| vsph2car | ||
| vsph2cyl | ||
| car2pol | ||
| pol2car | ||
| vcar2pol | ||
| vpol2car | ||
| Cython module | ||
| ------------- | ||
| .. autosummary:: | ||
| :toctree: | ||
| :template: cython-module | ||
| cython_special | ||
| """ | ||
| from scipy.special import ( # noqa: F401 | ||
| hankel1, | ||
| hankel2, | ||
| jv, | ||
| spherical_jn, | ||
| spherical_yn, | ||
| yv, | ||
| ) | ||
| from treams.special import _gufuncs, _ufuncs # noqa: F401 | ||
| from treams.special._gufuncs import * # noqa: F401, F403 | ||
| from treams.special._ufuncs import * # noqa: F401, F403 | ||
| def spherical_jn_d(n, z): | ||
| """Derivative of the spherical Bessel function of the first kind. | ||
| This is simply a wrapper for `scipy.special.spherical_jn(n, z, True)`, see | ||
| :py:func:`scipy.special.spherical_jn`. It's here to have a consistent way of | ||
| calling the derivative of a (spherical) Bessel or Hankel function. | ||
| Args: | ||
| n (int, array_like): Order | ||
| z (float or complex, array_like): Argument | ||
| Returns: | ||
| float or complex | ||
| References: | ||
| - `DLMF 10.47 <https://dlmf.nist.gov/10.47>`_ | ||
| - `DLMF 10.51 <https://dlmf.nist.gov/10.51>`_ | ||
| """ | ||
| return spherical_jn(n, z, True) | ||
| def spherical_yn_d(n, z): | ||
| """Derivative of the spherical Bessel function of the second kind. | ||
| This is simply a wrapper for `scipy.special.spherical_yn(n, z, True)`, see | ||
| :py:func:`scipy.special.spherical_jn`. It's here to have a consistent way of | ||
| calling the derivative of a (spherical) Bessel or Hankel function. | ||
| Args: | ||
| n (int, array_like): Order | ||
| z (float or complex, array_like): Argument | ||
| Returns: | ||
| float or complex | ||
| References: | ||
| - `DLMF 10.47 <https://dlmf.nist.gov/10.47>`_ | ||
| - `DLMF 10.51 <https://dlmf.nist.gov/10.51>`_ | ||
| """ | ||
| return spherical_yn(n, z, True) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| ctypedef fused number_t: | ||
| double complex | ||
| double | ||
| cpdef double complex hankel1_d(double l, double complex z) nogil | ||
| cpdef double complex hankel2_d(double l, double complex z) nogil | ||
| cpdef number_t jv_d(double l, number_t z) nogil | ||
| cpdef double complex spherical_hankel1(double n, number_t z) nogil | ||
| cpdef double complex spherical_hankel1_d(double l, double complex z) nogil | ||
| cpdef double complex spherical_hankel2(double n, number_t z) nogil | ||
| cpdef double complex spherical_hankel2_d(double l, double complex z) nogil | ||
| cpdef number_t yv_d(double l, number_t z) nogil | ||
| cdef void car2cyl(double *input, double *output) nogil | ||
| cdef void car2pol(double *input, double *output) nogil | ||
| cdef void car2sph(double *input, double *output) nogil | ||
| cdef void cyl2car(double *input, double *output) nogil | ||
| cdef void cyl2sph(double *input, double *output) nogil | ||
| cdef void pol2car(double *input, double *output) nogil | ||
| cdef void sph2car(double *input, double *output) nogil | ||
| cdef void sph2cyl(double *input, double *output) nogil | ||
| cdef void vcar2cyl(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vcar2pol(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vcar2sph(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vcyl2car(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vcyl2sph(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vpol2car(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vsph2car(number_t *iv, double *ip, number_t *ov) nogil | ||
| cdef void vsph2cyl(number_t *iv, double *ip, number_t *ov) nogil | ||
| cpdef number_t incgamma(double l, number_t z) nogil | ||
| cpdef number_t intkambe(long n, number_t z, number_t eta) nogil | ||
| cpdef number_t lpmv(double m, double l, number_t z) nogil | ||
| cpdef number_t pi_fun(double l, double m, number_t x) nogil | ||
| cpdef double complex sph_harm(double m, double l, double phi, number_t theta) nogil | ||
| cpdef number_t tau_fun(double l, double m, number_t x) nogil | ||
| cpdef double complex tl_vcw(double kz1, long mu, double kz2, long m, double complex krr, double phi, double z) nogil | ||
| cpdef double complex tl_vcw_r(double kz1, long mu, double kz2, long m, number_t krr, double phi, double z) nogil | ||
| cpdef double complex _tl_vsw_helper(long l, long m, long lambda_, long mu, long p, long q) nogil | ||
| cpdef double complex tl_vsw_A(long lambda_, long mu, long l, long m, double complex kr, number_t theta, double phi) nogil | ||
| cpdef double complex tl_vsw_B(long lambda_, long mu, long l, long m, double complex kr, number_t theta, double phi) nogil | ||
| cpdef double complex tl_vsw_rA(long lambda_, long mu, long l, long m, number_t kr, number_t theta, double phi) nogil | ||
| cpdef double complex tl_vsw_rB(long lambda_, long mu, long l, long m, number_t kr, number_t theta, double phi) nogil | ||
| cdef void vcw_A(double kz, long m, double complex krr, double phi, double z, double complex k, long pol, double complex *out) nogil | ||
| cdef void vcw_M(double kz, long m, double complex krr, double phi, double z, double complex *out) nogil | ||
| cdef void vcw_N(double kz, long m, double complex krr, double phi, double z, double complex k, double complex *out) nogil | ||
| cdef void vcw_rA(double kz, long m, number_t krr, double phi, double z, double complex k, long pol, double complex *out) nogil | ||
| cdef void vcw_rM(double kz, long m, number_t krr, double phi, double z, double complex *out, long i) nogil | ||
| cdef void vcw_rN(double kz, long m, number_t krr, double phi, double z, double complex k, double complex *out) nogil | ||
| cdef void vpw_A(number_t kx, number_t ky, number_t kz, double x, double y, double z, long pol, double complex *out) nogil | ||
| cdef void vpw_M(number_t kx, number_t ky, number_t kz, double x, double y, double z, double complex *out) nogil | ||
| cdef void vpw_N(number_t kx, number_t ky, number_t kz, double x, double y, double z, double complex *out) nogil | ||
| cdef void vsh_X(long l, long m, number_t theta, double phi, double complex *out) nogil | ||
| cdef void vsh_Y(long l, long m, number_t theta, double phi, double complex *out) nogil | ||
| cdef void vsh_Z(long l, long m, number_t theta, double phi, double complex *out) nogil | ||
| cdef void vsw_A(long l, long m, double complex kr, number_t theta, double phi, long pol, double complex *out) nogil | ||
| cdef void vsw_M(long l, long m, double complex kr, number_t theta, double phi, double complex *out) nogil | ||
| cdef void vsw_N(long l, long m, double complex kr, number_t theta, double phi, double complex *out) nogil | ||
| cdef void vsw_rA(long l, long m, number_t kr, number_t theta, double phi, long pol, double complex *out) nogil | ||
| cdef void vsw_rM(long l, long m, number_t kr, number_t theta, double phi, double complex *out) nogil | ||
| cdef void vsw_rN(long l, long m, number_t kr, number_t theta, double phi, double complex *out) nogil | ||
| cpdef number_t wignersmalld(long l, long m, long k, number_t theta) nogil | ||
| cpdef double complex wignerd(long l, long m, long k, double phi, number_t theta, double psi) nogil | ||
| cpdef double wigner3j(long j1, long j2, long j3, long m1, long m2, long m3) nogil |
Sorry, the diff of this file is too big to display
-1223
| """Utilities. | ||
| Collection of functions to make :class:`AnnotatedArray` work. The implementation is | ||
| aimed to be quite general, while still providing a solid basis for the rest of the code. | ||
| """ | ||
| import abc | ||
| import collections | ||
| import copy | ||
| import itertools | ||
| import warnings | ||
| import numpy as np | ||
| import treams._operators as _op | ||
| __all__ = [ | ||
| "AnnotatedArray", | ||
| "AnnotationDict", | ||
| "AnnotationError", | ||
| "AnnotationSequence", | ||
| "AnnotationWarning", | ||
| "implements", | ||
| "OrderedSet", | ||
| ] | ||
| class OrderedSet(collections.abc.Sequence, collections.abc.Set): | ||
| """Ordered set. | ||
| A abstract base class that combines a sequence and set. In contrast to regular sets | ||
| it is expected that the equality comparison only returns `True` if all entries are | ||
| in the same order. | ||
| """ | ||
| @abc.abstractmethod | ||
| def __eq__(self, other): | ||
| """Equality test.""" | ||
| raise NotImplementedError | ||
| class AnnotationWarning(UserWarning): | ||
| """Custom warning for Annotations. | ||
| By default the warning filter is set to 'always'. | ||
| """ | ||
| warnings.simplefilter("always", AnnotationWarning) | ||
| class AnnotationError(Exception): | ||
| """Custom exception for Annotations.""" | ||
| class AnnotationDict(collections.abc.MutableMapping): | ||
| """Dictionary that notifies the user when overwriting keys. | ||
| Behaves mostly similar to regular dictionaries, except that when overwriting | ||
| existing keys a :class:`AnnotationWarning` is emmitted. | ||
| Examples: | ||
| An :class:`AnnotationDict` can be created from other mappings, from a list of | ||
| key-value pairs (note how a warning is emmitted for the duplicate key), and from | ||
| keyword arguments. | ||
| .. code-block:: python | ||
| >>> AnnotationDict({"a": 1, "b": 2}) | ||
| AnnotationDict({'a': 1, 'b': 2}) | ||
| >>> AnnotationDict([("a", 1), ("b", 2), ("a", 3)]) | ||
| treams/util.py:74: AnnotationWarning: overwriting key 'a' | ||
| warnings.warn(f"overwriting key '{key}'", AnnotationWarning) | ||
| AnnotationDict({'a': 3, 'b': 2}) | ||
| >>> AnnotationDict({"a": 1, "b": 2}, c=3) | ||
| AnnotationDict({'a': 1, 'b': 2, 'c': 3}) | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| def __init__(self, items=(), /, **kwargs): | ||
| """Initialization.""" | ||
| self._dct = {} | ||
| for i in (items.items() if hasattr(items, "items") else items, kwargs.items()): | ||
| for key, val in i: | ||
| self[key] = val | ||
| def __getitem__(self, key): | ||
| """Get a value by its key. | ||
| Args: | ||
| key (hashable): Key | ||
| """ | ||
| return self._dct[key] | ||
| def __setitem__(self, key, val): | ||
| """Set item specified by key to the defined value. | ||
| When overwriting an existing key an :class:`AnnotationWarning` is emitted. | ||
| Avoid the warning by explicitly deleting the key first. | ||
| Args: | ||
| key (hashable): Key | ||
| val : Value | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| if key in self and self[key] != val: | ||
| warnings.warn(f"overwriting key '{key}'", AnnotationWarning) | ||
| self._dct[key] = val | ||
| def __delitem__(self, key): | ||
| """Delete the key.""" | ||
| del self._dct[key] | ||
| def __iter__(self): | ||
| """Iterate over the keys. | ||
| Returns: | ||
| Iterator | ||
| """ | ||
| return iter(self._dct) | ||
| def __len__(self): | ||
| """Number of keys contained. | ||
| Returns: | ||
| int | ||
| """ | ||
| return len(self._dct) | ||
| def __repr__(self): | ||
| """String representation. | ||
| Returns: | ||
| str | ||
| """ | ||
| return f"{self.__class__.__name__}({repr(self._dct)})" | ||
| def match(self, other): | ||
| """Compare the own keys to another dictionary. | ||
| This emits an :class:`AnnotationWarning` for each key that would be overwritten | ||
| by the given dictionary. | ||
| Args: | ||
| other (Mapping) | ||
| Returns: | ||
| None | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| for key, val in self.items(): | ||
| if key in other and other[key] != val: | ||
| warnings.warn(f"incompatible key '{key}'", AnnotationWarning) | ||
| class SequenceAsDict(collections.abc.MutableMapping): | ||
| def __get__(self, obj, objtype=None): | ||
| self._obj = obj | ||
| return self | ||
| def __set__(self, obj, dct): | ||
| self._obj = obj | ||
| for key, val in dct.items(): | ||
| self[key] = val | ||
| def __getitem__(self, key): | ||
| res = tuple(i.get(key) for i in self._obj) | ||
| if all(i is None for i in res): | ||
| raise KeyError(key) | ||
| return res | ||
| def __setitem__(self, key, val): | ||
| val = (None,) * (len(self._obj) - len(val)) + val | ||
| for dct, value in zip(reversed(self._obj), reversed(val)): | ||
| if value is None: | ||
| dct.pop(key, None) | ||
| else: | ||
| dct[key] = value | ||
| def __delitem__(self, key): | ||
| found = False | ||
| for dct in self._obj: | ||
| if key in dct: | ||
| del dct[key] | ||
| found = True | ||
| if not found: | ||
| raise KeyError(key) | ||
| def __iter__(self): | ||
| return iter({key for dct in self._obj for key in dct}) | ||
| def __len__(self): | ||
| return len({key for dct in self._obj for key in dct}) | ||
| def __repr__(self): | ||
| return f"{self.__class__.__name__}({dict(i for i in self.items())})" | ||
| class AnnotationSequence(collections.abc.Sequence): | ||
| """A Sequence of dictionaries. | ||
| This class is intended to work together with :class:`AnnotationDict`. It provides | ||
| convenience functions to interact with multiple of those dictionaries, which are | ||
| mainly used to keep track of the annotations made to each dimension of an | ||
| :class:`AnnotatedArray`. While the sequence itself is immutable the entries of each | ||
| dictionary is mutable. | ||
| Args: | ||
| *args: Items of the sequence | ||
| mapping (AnnotationDict): Type of mapping to use in the sequence. | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| as_dict = SequenceAsDict() | ||
| def __init__(self, *args, mapping=AnnotationDict): | ||
| """Initialization.""" | ||
| self._ann = tuple(mapping(i) for i in args) | ||
| def __len__(self): | ||
| """Number of dictionaries in the sequence. | ||
| Returns: | ||
| int | ||
| """ | ||
| return len(self._ann) | ||
| def __getitem__(self, key): | ||
| """Get an item or subsequence. | ||
| Indexing works with integers and slices like regular tuples. Additionally, it is | ||
| possible to get a copy of the object with `()`, or a new sequence of mappings in | ||
| a list (or other iterable) of integers. | ||
| Args: | ||
| key (iterable, slice, int) | ||
| Returns: | ||
| mapping | ||
| """ | ||
| if isinstance(key, tuple) and key == (): | ||
| return copy.copy(self._ann) | ||
| if isinstance(key, slice): | ||
| return type(self)(*self._ann[key]) | ||
| if isinstance(key, (int, np.integer)): | ||
| return self._ann[key] | ||
| res = [] | ||
| for k in key: | ||
| if not isinstance(k, int): | ||
| raise TypeError( | ||
| "sequence index must be integer, slice, list of integers, or '()'" | ||
| ) | ||
| res.append(self[k]) | ||
| return type(self)(*res) | ||
| def update(self, other): | ||
| """Update all mappings in the sequence at once. | ||
| The given sequence is aliged at the last entry and then updated pairwise. | ||
| Warnings for overwritten keys are extended by the information at which index | ||
| they occurred. | ||
| Args: | ||
| other (Sequence[Mapping]): Mappings to update with. | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| if len(other) > len(self): | ||
| warnings.warn( | ||
| f"argument of length {len(self)} given: " | ||
| f"ignore leading {len(other) - len(self)} entries", | ||
| AnnotationWarning, | ||
| ) | ||
| for i, (dest, src) in enumerate(zip(reversed(self), reversed(other))): | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error", category=AnnotationWarning) | ||
| try: | ||
| dest.update(src) | ||
| except AnnotationWarning as err: | ||
| warnings.simplefilter("always", category=AnnotationWarning) | ||
| warnings.warn( | ||
| f"at index {len(self) - i - 1}: " + err.args[0], | ||
| AnnotationWarning, | ||
| ) | ||
| def match(self, other): | ||
| """Match all mappings at once. | ||
| The given sequence is aliged at the last entry and then updated pairwise. | ||
| Warnings for overwritten keys are extended by the information at which index | ||
| they occurred, see also :func:`AnnotationDict.match`. | ||
| Args: | ||
| other (Sequence[Mappings]): Mappings to match. | ||
| Warns: | ||
| AnnotationWarning | ||
| """ | ||
| for i, (dest, src) in enumerate(zip(reversed(self), reversed(other))): | ||
| with warnings.catch_warnings(): | ||
| warnings.simplefilter("error", category=AnnotationWarning) | ||
| try: | ||
| dest.match(src) | ||
| except AnnotationWarning as err: | ||
| warnings.simplefilter("always", category=AnnotationWarning) | ||
| warnings.warn( | ||
| f"at dimension {len(self) + i}: " + err.args[0], | ||
| AnnotationWarning, | ||
| ) | ||
| def __eq__(self, other): | ||
| """Equality test. | ||
| Two sequences of mappings are considered equal when they have equal length and | ||
| when they all mappings are equal. | ||
| Args: | ||
| other (Sequence[Mapping]): Mappings to compare with. | ||
| Returns: | ||
| bool | ||
| """ | ||
| try: | ||
| lenother = len(other) | ||
| except TypeError: | ||
| return False | ||
| if len(self) != lenother: | ||
| return False | ||
| for a, b in zip(self, other): | ||
| if a != b: | ||
| return False | ||
| return True | ||
| def __repr__(self): | ||
| """String representation. | ||
| Returns: | ||
| str | ||
| """ | ||
| if len(self) == 1: | ||
| return f"{type(self).__name__}({repr(self._ann[0])})" | ||
| return f"{type(self).__name__}{repr(self._ann)}" | ||
| def __add__(self, other): | ||
| return AnnotationSequence(*self, *other) | ||
| def __radd__(self, other): | ||
| return AnnotationSequence(*other, *self) | ||
| def _cast_nparray(arr): | ||
| """Cast AnnotatedArray to numpy array.""" | ||
| return np.asarray(arr) if isinstance(arr, AnnotatedArray) else arr | ||
| def _cast_annarray(arr): | ||
| """Cast array to AnnotatedArray.""" | ||
| return arr if isinstance(arr, np.generic) else AnnotatedArray(arr) | ||
| def _parse_signature(signature, inputdims): | ||
| """Parse a ufunc signature based on the actual inputs. | ||
| The signature is matched with the input dimensions, to get the actual signature. It | ||
| is returned as two lists (inputs and outputs). Each list contains for each | ||
| individual input and output another list of the signature items. | ||
| Example: | ||
| >>> treams.util._parse_signature('(n?,k),(k,m?)->(n?,m?)', (2, 1)) | ||
| ([['n?', 'k'], ['k']], [['n?']]) | ||
| Args: | ||
| signature (str): Function signature | ||
| inputdims (Iterable[int]): Input dimensions | ||
| Returns: | ||
| tuple[list[list[str]] | ||
| """ | ||
| signature = "".join(signature.split()) # remove whitespace | ||
| sigin, sigout = signature.split("->") # split input and output | ||
| sigin = sigin[1:-1].split("),(") # split input | ||
| sigin = [i.split(",") for i in sigin] | ||
| sigout = sigout[1:-1].split("),(") # split output | ||
| sigout = [i.split(",") for i in sigout] | ||
| for i, idim in enumerate(inputdims): | ||
| j = 0 | ||
| while j < len(sigin[i]): | ||
| d = sigin[i][j] | ||
| if d.endswith("?") and len(sigin[i]) > idim: | ||
| sigin = [[i for i in s if i != d] for s in sigin] | ||
| sigout = [[i for i in s if i != d] for s in sigout] | ||
| else: | ||
| j += 1 | ||
| return sigin, sigout | ||
| def _parse_key(key, ndim): | ||
| """Parse a key to index an array. | ||
| This function attempts to replicate the numpy indexing of arrays. It can handle | ||
| integers, slices, an Ellipsis, arrays of integers, arrays of bools, (bare) bools, | ||
| and None. It returns the key with an Ellipsis appended if the number of index | ||
| dimensions does not match the number of dimensions given. Additionally, it informs | ||
| about the number of dimension indexed by fancy indexing, if the fancy indexed | ||
| dimensions will be prepended, and the number of dimensions that the Ellipsis | ||
| contains. | ||
| Args: | ||
| key (tuple): The indexing key | ||
| ndim (int): Number of array dimensions | ||
| Returns: | ||
| tuple | ||
| """ | ||
| consumed = 0 | ||
| ellipsis = False | ||
| fancy_ndim = 0 | ||
| # nfancy = 0 | ||
| consecutive_intfancy = 0 | ||
| # consecutive_intfancy = 0: no fancy/integer indexing | ||
| # consecutive_intfancy = 1: ongoing consecutive fancy/integer indexing | ||
| # consecutive_intfancy = 2: terminated consecutive fancy/integer indexing | ||
| # consecutive_intfancy >= 2: non-consecutive fancy/integer indexing | ||
| # The first pass gets the consumed dimensions, the presence of an ellipsis, and | ||
| # fancy index properties. | ||
| for k in key: | ||
| if k is not True and k is not False and isinstance(k, (int, np.integer)): | ||
| consumed += 1 | ||
| consecutive_intfancy += (consecutive_intfancy + 1) % 2 | ||
| elif k is None: | ||
| consecutive_intfancy += consecutive_intfancy % 2 | ||
| elif isinstance(k, slice): | ||
| consumed += 1 | ||
| consecutive_intfancy += consecutive_intfancy % 2 | ||
| elif k is Ellipsis: | ||
| ellipsis = True | ||
| # consumed is determined at the end | ||
| consecutive_intfancy += consecutive_intfancy % 2 | ||
| else: | ||
| arr = np.asanyarray(k) | ||
| if arr.dtype == bool: | ||
| consumed += arr.ndim | ||
| fancy_ndim = max(1, fancy_ndim) | ||
| else: | ||
| consumed += 1 | ||
| fancy_ndim = max(arr.ndim, fancy_ndim) | ||
| # nfancy += arr.ndim | ||
| consecutive_intfancy += (consecutive_intfancy + 1) % 2 | ||
| lenellipsis = ndim - consumed | ||
| if lenellipsis != 0 and not ellipsis: | ||
| key = key + (Ellipsis,) | ||
| return key, fancy_ndim, consecutive_intfancy >= 2, lenellipsis | ||
| HANDLED_FUNCTIONS = {} | ||
| """Dictionary of numpy functions implemented for AnnotatedArrays.""" | ||
| def implements(np_func): | ||
| """Decorator to register an __array_function__ implementation to AnnotatedArrays.""" | ||
| def decorator(func): | ||
| HANDLED_FUNCTIONS[np_func] = func | ||
| return func | ||
| return decorator | ||
| class AnnotatedArray(np.lib.mixins.NDArrayOperatorsMixin): | ||
| """Array that keeps track of annotations for each dimension. | ||
| This class acts mostly like numpy arrays, but it is enhanced by the following | ||
| functionalities: | ||
| * Annotations are added to each dimension | ||
| * Annotations are compared and preserved for numpy (generalized) | ||
| :py:class:`numpy.ufunc` (like :py:data:`numpy.add`, :py:data:`numpy.exp`, | ||
| :py:data:`numpy.matmul` and many more "standard" mathematical functions) | ||
| * Special ufunc methods, like :py:meth:`numpy.ufunc.reduce`, are supported | ||
| * A growing subset of other numpy functions are supported, like | ||
| :py:func:`numpy.linalg.solve` | ||
| * Keywords can be specified as scale are also index into, when index the | ||
| AnnotatedArray | ||
| * Annotations can also be exposed as properties | ||
| .. testsetup:: | ||
| from treams.util import AnnotatedArray | ||
| Example: | ||
| >>> a = AnnotatedArray([[0, 1], [2, 3]], ({"a": 1}, {"b": 2})) | ||
| >>> b = AnnotatedArray([1, 2], ({"b": 2},)) | ||
| >>> a @ b | ||
| AnnotatedArray( | ||
| [2, 8], | ||
| AnnotationSequence(AnnotationDict({'a': 1})), | ||
| ) | ||
| The interoperability with numpy is implemented using :ref:`basics.dispatch` | ||
| by defining :meth:`__array__`, :meth:`__array_ufunc__`, and | ||
| :meth:`__array_function__`. | ||
| """ | ||
| _scales = set() | ||
| def __init__(self, array, ann=(), /, **kwargs): | ||
| """Initalization.""" | ||
| self._array = np.asarray(array) | ||
| self.ann = getattr(array, "ann", ()) | ||
| self.ann.update(ann) | ||
| for key, val in kwargs.items(): | ||
| val = (val,) * self.ndim if not isinstance(val, tuple) else val | ||
| self.ann.as_dict[key] = val | ||
| @classmethod | ||
| def relax(cls, *args, mro=None, **kwargs): | ||
| """Try creating AnnotatedArray subclasses if possible. | ||
| Subclasses can impose stricter conditions on the Annotations. To allow a simple | ||
| "decaying" of those subclasses it is possible to create them with this | ||
| classmethod. It attempts array creations along the method resolution order until | ||
| it succeeds. | ||
| Args: | ||
| mro (array-like, optional): Method resolution order along which to create | ||
| the subclass. By default it takes the order of the calling class. | ||
| Note: | ||
| All other arguments are the same as for the default initialization. | ||
| """ | ||
| mro = cls.__mro__[1:] if mro is None else mro | ||
| try: | ||
| return cls(*args, **kwargs) | ||
| except AnnotationError as err: | ||
| if cls == AnnotatedArray: | ||
| raise err from None | ||
| cls, *mro = mro | ||
| return cls.relax(*args, mro=mro, **kwargs) | ||
| def __str__(self): | ||
| """String of the array itself.""" | ||
| return str(self._array) | ||
| def __repr__(self): | ||
| """String representation.""" | ||
| repr_arr = " " + repr(self._array)[6:-1].replace("\n ", "\n") | ||
| return f"{self.__class__.__name__}(\n{repr_arr},\n {self.ann},\n)" | ||
| def __int__(self): | ||
| return int(self._array) | ||
| def __float__(self): | ||
| return float(self._array) | ||
| def __complex__(self): | ||
| return complex(self._array) | ||
| def __array__(self, dtype=None): | ||
| """Convert to an numpy array. | ||
| This function returns the bare array without annotations. This function does not | ||
| necessarily make a copy of the array. | ||
| Args: | ||
| dtype (optional): Type of the returned array. | ||
| """ | ||
| return np.asarray(self._array, dtype=dtype) | ||
| @property | ||
| def ann(self): | ||
| """Array annotations.""" | ||
| return self._ann | ||
| @ann.setter | ||
| def ann(self, ann): | ||
| """Set array annotations. | ||
| This function copies the given sequence of dictionaries. | ||
| """ | ||
| self._ann = AnnotationSequence(*(({},) * self.ndim)) | ||
| self._ann.update(ann) | ||
| def __getattr__(self, key): | ||
| # In most cases, we shouldn't arrive here with the key "_ann", an exception is | ||
| # pickle.load which needs this early error to not result in an infinite | ||
| # recursion | ||
| if key == "_ann": | ||
| raise AttributeError() | ||
| if key in self._ann.as_dict: | ||
| res = self._ann.as_dict[key] | ||
| if all(res[0] == i for i in res[1:]): | ||
| return res[0] | ||
| return res | ||
| raise AttributeError( | ||
| f"'{self.__class__.__name__}' object has no attribute '{key}'" | ||
| ) | ||
| def __setattr__(self, key, val): | ||
| if key not in ("_array", "ann", "_ann") and key in self.ann.as_dict: | ||
| val = (val,) * self.ndim if not isinstance(val, tuple) else val | ||
| self.ann.as_dict[key] = val | ||
| else: | ||
| super().__setattr__(key, val) | ||
| def __delattr__(self, key): | ||
| try: | ||
| super().__delattr__(key) | ||
| except AttributeError: | ||
| del self.ann.as_dict[key] | ||
| def __len__(self): | ||
| return len(self._array) | ||
| def __copy__(self): | ||
| return type(self)(copy.copy(self._array), copy.copy(self._ann)) | ||
| def __deepcopy__(self, memo): | ||
| cls = type(self) | ||
| return cls(copy.deepcopy(self._array, memo), copy.deepcopy(self._ann, memo)) | ||
| def __bool__(self): | ||
| """Boolean value of the array.""" | ||
| return bool(self._array) | ||
| def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): | ||
| """Implement ufunc API.""" | ||
| # Compute result first to use numpy's comprehensive checks on the arguments | ||
| inputs_noaa = tuple(map(_cast_nparray, inputs)) | ||
| ann_out = () | ||
| out = kwargs.get("out") | ||
| out = out if isinstance(out, tuple) else (out,) | ||
| ann_out = tuple(getattr(i, "ann", None) for i in out) | ||
| if len(out) != 1 or out[0] is not None: | ||
| kwargs["out"] = tuple(map(_cast_nparray, out)) | ||
| res = getattr(ufunc, method)(*inputs_noaa, **kwargs) | ||
| istuple, res = (True, res) if isinstance(res, tuple) else (False, (res,)) | ||
| res = tuple(_cast_annarray(r) if o is None else o for r, o in zip(res, out)) | ||
| for a, r in zip(ann_out, res): | ||
| if a is not None: | ||
| r.ann.update(a) | ||
| inputs_and_where = inputs + ((kwargs["where"],) if "where" in kwargs else ()) | ||
| if ufunc.signature is None or all( | ||
| map(lambda x: x in " (),->", ufunc.signature) | ||
| ): | ||
| if method in ("__call__", "reduceat", "accumulate") or ( | ||
| method == "reduce" and kwargs.get("keepdims", False) | ||
| ): | ||
| self._ufunc_call(inputs_and_where, res) | ||
| elif method == "reduce": | ||
| self._ufunc_reduce(inputs_and_where, res, kwargs.get("axis", 0)) | ||
| elif method == "at": | ||
| self._ufunc_at(inputs) | ||
| return None | ||
| elif method == "outer": | ||
| self._ufunc_outer(inputs, res, kwargs.get("where", True)) | ||
| else: | ||
| warnings.warn("unrecognized ufunc method", AnnotationWarning) | ||
| else: | ||
| res = self._gufunc_call(ufunc, inputs, kwargs, res) | ||
| res = tuple(r if isinstance(r, np.generic) else self.relax(r) for r in res) | ||
| return res if istuple else res[0] | ||
| @staticmethod | ||
| def _ufunc_call(inputs_and_where, res): | ||
| for out, in_ in itertools.product(res, inputs_and_where): | ||
| if not isinstance(out, AnnotatedArray): | ||
| continue | ||
| out.ann.update(getattr(in_, "ann", ())) | ||
| @staticmethod | ||
| def _ufunc_reduce(inputs_and_where, res, axis=0, keepdims=False): | ||
| out = res[0] # reduce only allowed for single output functions | ||
| if not isinstance(out, AnnotatedArray): | ||
| return | ||
| axis = axis if isinstance(axis, tuple) else (axis,) | ||
| in_ = inputs_and_where[0] | ||
| axis = sorted(map(lambda x: x % np.ndim(in_) - np.ndim(in_), axis)) | ||
| for in_ in inputs_and_where: | ||
| ann = list(getattr(in_, "ann", [])) | ||
| if not keepdims: | ||
| for a in axis: | ||
| try: | ||
| del ann[a] | ||
| except IndexError: | ||
| pass | ||
| out.ann.update(ann) | ||
| @staticmethod | ||
| def _ufunc_at(inputs): | ||
| out = inputs[0] | ||
| if not isinstance(out, AnnotatedArray): | ||
| return | ||
| if any(d != {} for d in getattr(inputs[1], "ann", ())): | ||
| warnings.warn("annotations in indices are ignored", AnnotationWarning) | ||
| for in_ in inputs[2:]: | ||
| ann = getattr(in_, "ann", ()) | ||
| out.ann.update(ann) | ||
| @staticmethod | ||
| def _ufunc_outer(inputs, res, where=True): | ||
| for out in res: | ||
| if not isinstance(out, AnnotatedArray): | ||
| continue | ||
| in_ann = tuple( | ||
| i for a in inputs for i in getattr(a, "ann", np.ndim(a) * ({},)) | ||
| ) | ||
| out.ann.update(in_ann) | ||
| where_ann = getattr(where, "ann", ()) | ||
| out.ann.update(where_ann) | ||
| @staticmethod | ||
| def _gufunc_call(ufunc, inputs, kwargs, res): | ||
| sigin, sigout = _parse_signature(ufunc.signature, map(np.ndim, inputs)) | ||
| if kwargs.get("keepdims", False): | ||
| sigout = [sigin[0] for _ in range(ufunc.nout)] | ||
| ndims = [np.ndim(i) for x in (inputs, res) for i in x] | ||
| axes = getattr(kwargs, "axes", None) | ||
| if axes is None: | ||
| axis = getattr(kwargs, "axis", None) | ||
| if axis is None: | ||
| axes = [tuple(range(-len(i), 0)) for i in sigin + sigout] | ||
| else: | ||
| axes = [(axis,) for _ in range(ufunc.nin)] | ||
| else: | ||
| axes = [ | ||
| tuple(a) if isinstance(a, collections.abc.Iterable) else (a,) | ||
| for a in axes | ||
| ] | ||
| append = axes[0] if kwargs.get("keepdims", False) else () | ||
| axes += [append] * (ufunc.nin + ufunc.nout - len(axes)) | ||
| axes = [(*(i % ndim - ndim for i in a),) for a, ndim in zip(axes, ndims)] | ||
| iterdims = [ | ||
| [i for i in range(-1, -1 - ndim, -1) if i not in a] | ||
| for a, ndim in zip(axes, ndims) | ||
| ] | ||
| # compare core dimensions | ||
| coredims = {} | ||
| for i, (ax, sig) in enumerate(zip(axes, sigin + sigout)): | ||
| for a, key in zip(ax, sig): | ||
| if key.isnumeric(): | ||
| continue | ||
| coredims.setdefault(key, []).append((i, a)) | ||
| inout = tuple(inputs) + res | ||
| for val in coredims.values(): | ||
| for (isrc, dimsrc), (idest, dimdest) in itertools.combinations(val, 2): | ||
| source = getattr(inout[isrc], "ann", {dimsrc: {}})[dimsrc] | ||
| dest = getattr(inout[idest], "ann", {dimdest: AnnotationDict()})[ | ||
| dimdest | ||
| ] | ||
| if isrc < ufunc.nin <= idest: | ||
| dest.update(source) | ||
| else: | ||
| dest.match(source) | ||
| # compare iteration dimensions | ||
| for iout, out in enumerate(res): | ||
| if not isinstance(out, AnnotatedArray): | ||
| continue | ||
| for idim, dim in enumerate(iterdims[ufunc.nin + iout]): | ||
| dest = out.ann[dim] | ||
| for in_, iterdim in zip(inputs, iterdims): | ||
| if idim >= len(iterdim) or getattr(in_, "ann", None) is None: | ||
| continue | ||
| source = getattr(in_, "ann", {iterdim[idim]: {}})[iterdim[idim]] | ||
| dest.update(source) | ||
| return res | ||
| def __array_function__(self, func, types, args, kwargs): | ||
| """Function calls on the array. | ||
| Calls defined function in :data:`HANDLED_FUNCTIONS` otherwise raises exception. | ||
| Add functions to it by using the decorator :func:`implements` for custom | ||
| implementations. | ||
| """ | ||
| if func not in HANDLED_FUNCTIONS: | ||
| return NotImplemented | ||
| # if not all(issubclass(t, self.__class__) for t in types): | ||
| # return NotImplemented | ||
| return HANDLED_FUNCTIONS[func](*args, **kwargs) | ||
| def __getitem__(self, key): | ||
| """Get an item from the AnnotatedArray. | ||
| The indexing supports most of numpys regular and fancy indexing. | ||
| """ | ||
| res = AnnotatedArray(self._array[key]) | ||
| if isinstance(res, np.generic) or res.ndim == 0: | ||
| return res | ||
| key = key if isinstance(key, tuple) else (key,) | ||
| key, fancy_ndim, prepend_fancy, lenellipsis = _parse_key(key, self.ndim) | ||
| source = 0 | ||
| dest = fancy_ndim if prepend_fancy else 0 | ||
| for k in key: | ||
| if k is not True and k is not False and isinstance(k, (int, np.integer)): | ||
| source += 1 | ||
| elif k is None: | ||
| dest += 1 | ||
| elif isinstance(k, slice): | ||
| for kk, val in self.ann[source].items(): | ||
| if kk in self._scales: | ||
| res.ann[dest][kk] = val[k] | ||
| else: | ||
| res.ann[dest][kk] = val | ||
| dest += 1 | ||
| source += 1 | ||
| elif k is Ellipsis: | ||
| for _ in range(lenellipsis): | ||
| res.ann[dest].update(self.ann[source]) | ||
| dest += 1 | ||
| source += 1 | ||
| else: | ||
| k = np.asanyarray(k) | ||
| ksq = k.squeeze() | ||
| if ksq.ndim == 1: | ||
| pos = (np.array(k.shape) == k.size).argmax() | ||
| ann = ( | ||
| self.ann[source + pos] if k.dtype == bool else self.ann[source] | ||
| ) | ||
| pos += int(not prepend_fancy) * dest + fancy_ndim - k.ndim | ||
| for kk, val in ann.items(): | ||
| if kk in self._scales: | ||
| res.ann[pos][kk] = val[ksq] | ||
| else: | ||
| res.ann[pos][kk] = val | ||
| source += k.ndim if k.dtype == bool else 1 | ||
| if not prepend_fancy: | ||
| dest += fancy_ndim | ||
| fancy_ndim = 0 | ||
| return self.relax(res) | ||
| def __setitem__(self, key, value): | ||
| """Set values. | ||
| If the provided value is an AnnotatedArray the annotations of corresponding | ||
| dimensions will be matched. | ||
| """ | ||
| self._array[key] = value | ||
| if not hasattr(value, "ann"): | ||
| return | ||
| key = key if isinstance(key, tuple) else (key,) | ||
| key, fancy_ndim, prepend_fancy, lenellipsis = _parse_key(key, self.ndim) | ||
| source = 0 | ||
| dest = fancy_ndim if prepend_fancy else 0 | ||
| for k in key: | ||
| if k is not True and k is not False and isinstance(k, (int, np.integer)): | ||
| source += 1 | ||
| elif k is None: | ||
| dest += 1 | ||
| elif isinstance(k, slice): | ||
| for kk, val in self.ann[source].items(): | ||
| if kk not in value.ann[dest]: | ||
| continue | ||
| if kk in self._scales: | ||
| val = val[k] | ||
| if val != value.ann[dest][kk]: | ||
| warnings.warn( | ||
| f"incompatible annotations with key '{kk}' " | ||
| f"comparing dimensions '{source}' and '{dest}'", | ||
| AnnotationWarning, | ||
| ) | ||
| dest += 1 | ||
| source += 1 | ||
| elif k is Ellipsis: | ||
| for _ in range(lenellipsis): | ||
| self.ann[source].match(value.ann[dest]) | ||
| dest += 1 | ||
| source += 1 | ||
| else: | ||
| k = np.asanyarray(k) | ||
| ksq = k.squeeze() | ||
| if ksq.ndim == 1: | ||
| pos = (np.array(k.shape) == k.size).argmax() | ||
| ann = ( | ||
| self.ann[source + pos] if k.dtype == bool else self.ann[source] | ||
| ) | ||
| pos += int(not prepend_fancy) * dest + fancy_ndim - k.ndim | ||
| for kk, val in ann.items(): | ||
| if kk not in value.ann[pos]: | ||
| continue | ||
| if kk in self._scales: | ||
| val = val[k] | ||
| if val != value.ann[pos][kk]: | ||
| warnings.warn( | ||
| f"incompatible annotations with key '{kk}' " | ||
| f"comparing dimensions '{source}' and '{pos}'", | ||
| AnnotationWarning, | ||
| ) | ||
| source += k.ndim if k.dtype == bool else 1 | ||
| if not prepend_fancy: | ||
| dest += fancy_ndim | ||
| fancy_ndim = 0 | ||
| @property | ||
| def T(self): | ||
| """Transpose. | ||
| See also :py:attr:`numpy.ndarray.T`. | ||
| """ | ||
| return self.transpose() | ||
| @implements(np.all) | ||
| def all(self, axis=None, dtype=None, out=None, keepdims=False, *, where=True): | ||
| """Test if all elements (along an axis) are True. | ||
| See also :py:meth:`numpy.ndarray.all`. | ||
| """ | ||
| return np.logical_and.reduce(self, axis, dtype, out, keepdims, where=where) | ||
| @implements(np.any) | ||
| def any(self, axis=None, dtype=None, out=None, keepdims=False, *, where=True): | ||
| """Test if any element (along an axis) is True. | ||
| See also :py:meth:`numpy.ndarray.any`. | ||
| """ | ||
| return np.logical_or.reduce(self, axis, dtype, out, keepdims, where=where) | ||
| @implements(np.max) | ||
| def max(self, axis=None, out=None, keepdims=False, initial=None, where=True): | ||
| """Maximum (along an axis). | ||
| See also :py:meth:`numpy.ndarray.max`. | ||
| """ | ||
| return np.maximum.reduce(self, axis, None, out, keepdims, initial, where) | ||
| @implements(np.min) | ||
| def min(self, axis=None, out=None, keepdims=False, initial=None, where=True): | ||
| """Minimum (along an axis). | ||
| See also :py:meth:`numpy.ndarray.min`. | ||
| """ | ||
| return np.minimum.reduce(self, axis, None, out, keepdims, initial, where) | ||
| @implements(np.sum) | ||
| def sum( | ||
| self, axis=None, dtype=None, out=None, keepdims=False, initial=0, where=True | ||
| ): | ||
| """Sum of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.sum`. | ||
| """ | ||
| return np.add.reduce(self, axis, dtype, out, keepdims, initial, where) | ||
| @implements(np.prod) | ||
| def prod( | ||
| self, axis=None, dtype=None, out=None, keepdims=False, initial=1, where=True | ||
| ): | ||
| """Product of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.prod`. | ||
| """ | ||
| return np.multiply.reduce(self, axis, dtype, out, keepdims, initial, where) | ||
| @implements(np.cumsum) | ||
| def cumsum(self, axis=None, dtype=None, out=None): | ||
| """Cumulative sum of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.cumsum`. | ||
| """ | ||
| if axis is None: | ||
| return np.add.accumulate(self.flatten(), 0, dtype, out) | ||
| return np.add.accumulate(self, axis, dtype, out) | ||
| @implements(np.cumprod) | ||
| def cumprod(self, axis=None, dtype=None, out=None): | ||
| """Cumulative product of elements (along an axis). | ||
| See also :py:meth:`numpy.ndarray.cumprod`. | ||
| """ | ||
| if axis is None: | ||
| return np.multiply.accumulate(self.flatten(), 0, dtype, out) | ||
| return np.multiply.accumulate(self, axis, dtype, out) | ||
| def flatten(self, order="C"): | ||
| """Flatten array to one dimension. | ||
| See also :py:meth:`numpy.ndarray.flatten`. | ||
| """ | ||
| res = self.relax(self._array.flatten(order)) | ||
| if res.shape == self.shape: | ||
| res.ann.update(self.ann) | ||
| elif len(tuple(filter(lambda x: x != 1, self.shape))) == 1: | ||
| res.ann[0].update(self.ann[self.shape.index(res.size)]) | ||
| return res | ||
| @implements(np.trace) | ||
| def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): | ||
| """Trace of an array. | ||
| See also :py:meth:`numpy.ndarray.trac`. | ||
| """ | ||
| ann = tuple(a for i, a in enumerate(self.ann) if i not in (axis1, axis2)) | ||
| return self.relax(self._array.trace(offset, axis1, axis2, dtype, out), ann) | ||
| def astype(self, *args, **kwargs): | ||
| """Return array as given type. | ||
| See also :py:meth:`numpy.ndarray.astype`. | ||
| """ | ||
| return self.relax(self._array.astype(*args, **kwargs), self.ann) | ||
| @property | ||
| @implements(np.ndim) | ||
| def ndim(self): | ||
| """Number of array dimensions. | ||
| See also :py:attr:`numpy.ndarray.ndim`. | ||
| """ | ||
| return self._array.ndim | ||
| @property | ||
| @implements(np.shape) | ||
| def shape(self): | ||
| """Array shape. | ||
| See also :py:attr:`numpy.ndarray.shape`. | ||
| """ | ||
| return self._array.shape | ||
| @property | ||
| @implements(np.size) | ||
| def size(self): | ||
| """Array size. | ||
| See also :py:attr:`numpy.ndarray.size`. | ||
| """ | ||
| return self._array.size | ||
| @property | ||
| @implements(np.imag) | ||
| def imag(self): | ||
| """Imaginary part of the array. | ||
| See also :py:attr:`numpy.ndarray.imag`. | ||
| """ | ||
| return self.relax(self._array.imag, self.ann) | ||
| @property | ||
| @implements(np.real) | ||
| def real(self): | ||
| """Real part of the array. | ||
| See also :py:attr:`numpy.ndarray.real`. | ||
| """ | ||
| return self.relax(self._array.real, self.ann) | ||
| def conjugate(self, *args, **kwargs): | ||
| """Complex conjugate elementwise. | ||
| See also :py:meth:`numpy.ndarray.conjugate`. | ||
| """ | ||
| return np.conjugate(self, *args, **kwargs) | ||
| @implements(np.diagonal) | ||
| def diagonal(self, offset=0, axis1=0, axis2=1): | ||
| """Get the diagonal of the array. | ||
| See also :py:meth:`numpy.ndarray.diagonal`. | ||
| """ | ||
| ann = tuple(a for i, a in enumerate(self.ann) if i not in (axis1, axis2)) + ( | ||
| {}, | ||
| ) | ||
| return self.relax(self._array.diagonal(offset, axis1, axis2), ann) | ||
| @implements(np.transpose) | ||
| def transpose(self, axes=None): | ||
| """Transpose array dimensions. | ||
| See also :py:meth:`numpy.ndarray.transpose`. | ||
| """ | ||
| axes = range(self.ndim - 1, -1, -1) if axes is None else axes | ||
| return self.relax(self._array.transpose(axes), self.ann[axes]) | ||
| conj = conjugate | ||
| @implements(np.swapaxes) | ||
| def swapaxes(self, axis1, axis2): | ||
| axis1 = axis1 % self.ndim | ||
| axis2 = axis2 % self.ndim | ||
| opp = {axis1: axis2, axis2: axis1} | ||
| axes = [opp.get(i, i) for i in range(self.ndim)] | ||
| return self.relax(self._array.swapaxes(axis1, axis2), self.ann[axes]) | ||
| def __matmul__(self, other): | ||
| if isinstance(other, _op.Operator): | ||
| return NotImplemented | ||
| return super().__matmul__(other) | ||
| @implements(np.linalg.solve) | ||
| def solve(a, b): | ||
| """Solve linear system. | ||
| See also :py:func:`numpy.linalg.solve`. | ||
| """ | ||
| if issubclass(type(a), type(b)) or ( | ||
| not issubclass(type(b), type(a)) and isinstance(a, AnnotatedArray) | ||
| ): | ||
| restype = type(a) | ||
| else: | ||
| restype = type(b) | ||
| res = AnnotatedArray(np.linalg.solve(np.asanyarray(a), np.asanyarray(b))) | ||
| a_ann = list(getattr(a, "ann", [{}, {}])) | ||
| b_ann = list(getattr(b, "ann", [{}, {}])) | ||
| if np.ndim(b) == np.ndim(a) - 1: | ||
| map(lambda x: x[0].match(x[1]), zip(a_ann[-2::-1], b_ann[-1::-1])) | ||
| del a_ann[-2] | ||
| del b_ann[-1] | ||
| else: | ||
| map(lambda x: x[0].match(x[1]), zip(a_ann[-2::-1], b_ann[-2::-1])) | ||
| del a_ann[-2] | ||
| del b_ann[-2] | ||
| a_ann += [{}] | ||
| res.ann.update(a_ann) | ||
| res.ann.update(b_ann) | ||
| return restype.relax(res) | ||
| @implements(np.linalg.lstsq) | ||
| def lstsq(a, b, rcond="warn"): | ||
| """Solve linear system using least squares. | ||
| See also :py:func:`numpy.linalg.lstsq`. | ||
| """ | ||
| if issubclass(type(a), type(b)) or ( | ||
| not issubclass(type(b), type(a)) and isinstance(a, AnnotatedArray) | ||
| ): | ||
| restype = type(a) | ||
| else: | ||
| restype = type(b) | ||
| res = list(np.linalg.lstsq(np.asanyarray(a), np.asanyarray(b), rcond)) | ||
| res[0] = AnnotatedArray(res[0]) | ||
| a_ann = getattr(a, "ann", ({},)) | ||
| b_ann = getattr(b, "ann", ({},)) | ||
| a_ann[0].match(b_ann[0]) | ||
| res[0].ann[0].update(a_ann[-1]) | ||
| if np.ndim(b) == 2: | ||
| res[0].ann[1].update(b_ann[-1]) | ||
| res[0] = restype.relax(res[0]) | ||
| return tuple(res) | ||
| @implements(np.linalg.svd) | ||
| def svd(a, full_matrices=True, compute_uv=True, hermitian=False): | ||
| """Compute the singular value decomposition. | ||
| See also :py:func:`numpy.linalg.svd`. | ||
| """ | ||
| res = list(np.linalg.svd(np.asanyarray(a), full_matrices, compute_uv, hermitian)) | ||
| ann = getattr(a, "ann", ({},)) | ||
| if compute_uv: | ||
| res[0] = a.relax(res[0], ann[:-1] + ({},)) | ||
| res[1] = a.relax(res[1], ann[:-2] + ({},)) | ||
| res[2] = a.relax(res[2], ann[:-2] + ({}, ann[-1])) | ||
| return res | ||
| return a.relax(res, tuple(ann[:-2]) + ({},)) | ||
| @implements(np.diag) | ||
| def diag(a, k=0): | ||
| """Extract diagonal from an array or create an diagonal array. | ||
| See also :py:func:`numpy.diag`. | ||
| """ | ||
| res = np.diag(np.asanyarray(a), k) | ||
| ann = a.ann | ||
| if a.ndim == 1: | ||
| ann = (ann[0], copy.copy(ann[0])) | ||
| elif k == 0: | ||
| ann[0].match(ann[1]) | ||
| ann = ({**ann[0], **ann[1]},) | ||
| else: | ||
| ann = () | ||
| return a.relax(res, ann) | ||
| @implements(np.tril) | ||
| def tril(a, k=0): | ||
| """Get lower trinagular matrix. | ||
| See also :py:func:`numpy.tril`. | ||
| """ | ||
| res = np.tril(np.asanyarray(a), k) | ||
| return a.relax(res, getattr(a, "ann", ({},))) | ||
| @implements(np.triu) | ||
| def triu(a, k=0): | ||
| """Get upper trinagular matrix. | ||
| See also :py:func:`numpy.triu`. | ||
| """ | ||
| res = np.triu(np.asanyarray(a), k) | ||
| return a.relax(res, getattr(a, "ann", ({},))) | ||
| @implements(np.zeros_like) | ||
| def zeros_like(a, dtype=None, order="K", shape=None): | ||
| """Create a numpy array like the given array containing zeros.""" | ||
| return np.zeros_like(np.asarray(a), dtype=dtype, order=order, shape=shape) | ||
| @implements(np.ones_like) | ||
| def ones_like(a, dtype=None, order="K", shape=None): | ||
| """Create a numpy array like the given array containing ones.""" | ||
| return np.ones_like(np.asarray(a), dtype=dtype, order=order, shape=shape) |
| import numpy as np | ||
| import pytest | ||
| import treams.lattice as la | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| def osscilation_avg(res): | ||
| avg_len = len(res) // 2 | ||
| return np.mean(np.cumsum(res)[-avg_len:]) | ||
| EPS = 2e-7 | ||
| EPSSQ = 4e-14 | ||
| class TestLSumSW1d: | ||
| @pytest.mark.parametrize("l", [0, 1, 2, 7, 8]) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [-0.2, 0, 0.7]) | ||
| @pytest.mark.parametrize("a", [1, 1.1]) | ||
| @pytest.mark.parametrize("r", [-0.2, 0, 0.5]) | ||
| def test_regular(self, l, k, kpar, a, r): # noqa: E741 | ||
| assert isclose( | ||
| la.lsumsw1d(l, k, kpar, a, r, 0), | ||
| np.sum(la.dsumsw1d(l, k, kpar, a, r, np.arange(100_000))), | ||
| rel_tol=0.02, | ||
| abs_tol=EPSSQ, | ||
| ) | ||
| @pytest.mark.parametrize("l", [0, 1, 2, 7, 8]) | ||
| @pytest.mark.parametrize("r", [-0.2, 0, 0.5]) | ||
| @pytest.mark.slow | ||
| def test_singular(self, l, r): # noqa: E741 | ||
| a = 2 * np.pi | ||
| k = 1 | ||
| kpar = 0 | ||
| assert isclose( | ||
| la.lsumsw1d(l, k, kpar, a, r, 0), | ||
| np.sum(la.dsumsw1d(l, k, kpar, a, r, np.arange(1_300_000))), | ||
| rel_tol=0.05, | ||
| abs_tol=EPSSQ, | ||
| ) | ||
| class TestLSumCW1d: | ||
| @pytest.mark.parametrize("l", [-2, -1, 0, 7, 8]) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j, 2j]) | ||
| @pytest.mark.parametrize("kpar", [-0.2, 0, 0.7]) | ||
| @pytest.mark.parametrize("a", [1, 1.1]) | ||
| @pytest.mark.parametrize("r", [-0.2, 0, 0.5]) | ||
| def test_regular(self, l, k, kpar, a, r): # noqa: E741 | ||
| assert isclose( | ||
| la.lsumcw1d(l, k, kpar, a, r, 0), | ||
| np.sum(la.dsumcw1d(l, k, kpar, a, r, np.arange(100_000))), | ||
| rel_tol=0.02, | ||
| abs_tol=EPSSQ, | ||
| ) | ||
| @pytest.mark.parametrize("l", [-2, -1, 0, 7, 8]) | ||
| @pytest.mark.parametrize("r", [-0.2, 0, 0.5]) | ||
| def test_singular(self, l, r): # noqa: E741 | ||
| a = 2 * np.pi | ||
| k = 1 | ||
| kpar = 0 | ||
| assert isclose( | ||
| la.lsumcw1d(l, k, kpar, a, r, 0), | ||
| np.sum(la.dsumcw1d(l, k, kpar, a, r, np.arange(2_000_000))), | ||
| rel_tol=0.05, | ||
| abs_tol=EPSSQ, | ||
| ) | ||
| class TestLSumCW2d: | ||
| @pytest.mark.parametrize("l", [-2, -1, 0, 7, 8]) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [np.zeros(2), np.array([0.1, 0.2])]) | ||
| @pytest.mark.parametrize( | ||
| "a", [np.array([[1, 0], [0, 1.2]]), np.array([[1.1, 0.2], [-0.1, 1]])] | ||
| ) | ||
| @pytest.mark.parametrize("r", [np.zeros(2), np.array([0.2, 0])]) | ||
| @pytest.mark.slow | ||
| def test_regular(self, l, k, kpar, a, r): # noqa: E741 | ||
| assert isclose( | ||
| la.lsumcw2d(l, k, kpar, a, r, 0), | ||
| osscilation_avg(la.dsumcw2d(l, k, kpar, a, r, np.arange(800))), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize("l", [-2, -1, 0, 7, 8]) | ||
| @pytest.mark.parametrize("r", [np.zeros(2), np.array([0.2, 0])]) | ||
| @pytest.mark.slow | ||
| def test_singular(self, l, r): # noqa: E741 | ||
| k = 1 | ||
| kpar = np.zeros(2) | ||
| a = np.array([[2 * np.pi, 0], [0, 1]]) | ||
| # Here the direct result is too oscillatory | ||
| if l == -2: # noqa: E741 | ||
| if np.all(r == 0): | ||
| assert isclose( | ||
| np.imag(la.lsumcw2d(l, k, kpar, a, r, 0)), 2.9105962449216736 | ||
| ) | ||
| elif l == 0: # noqa: E741 | ||
| if np.all(r == 0): | ||
| assert isclose( | ||
| np.imag(la.lsumcw2d(l, k, kpar, a, r, 0)), 0.9156690769853802 | ||
| ) | ||
| else: | ||
| assert isclose( | ||
| np.imag(la.lsumcw2d(l, k, kpar, a, r, 0)), -0.15877332685629297 | ||
| ) | ||
| else: | ||
| assert isclose( | ||
| np.imag(la.lsumcw2d(l, k, kpar, a, r, 0)), | ||
| np.imag(np.sum(la.dsumcw2d(l, k, kpar, a, r, np.arange(1000)))), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| class TestLSumSW2d: | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 0), | ||
| (2, 1), | ||
| (7, -7), | ||
| (7, 0), | ||
| (7, 3), | ||
| (8, -8), | ||
| (8, 0), | ||
| (8, 4), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [np.zeros(2), np.array([0.1, 0.2])]) | ||
| @pytest.mark.parametrize( | ||
| "a", [np.array([[1, 0], [0, 1.2]]), np.array([[1.1, 0.2], [-0.1, 1]])] | ||
| ) | ||
| @pytest.mark.parametrize("r", [np.zeros(2), np.array([0.2, 0])]) | ||
| @pytest.mark.slow | ||
| def test_regular(self, lm, k, kpar, a, r): | ||
| assert isclose( | ||
| la.lsumsw2d(*lm, k, kpar, a, r, 0), | ||
| osscilation_avg(la.dsumsw2d(*lm, k, kpar, a, r, np.arange(800))), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize("lm", [(0, 0), (2, -2), (2, 0), (8, -8), (8, 0), (8, 4)]) | ||
| @pytest.mark.slow | ||
| def test_singular_r0(self, lm): | ||
| k = 1 | ||
| kpar = np.zeros(2) | ||
| a = np.array([[2 * np.pi, 0], [0, 1]]) | ||
| r = np.zeros(2) | ||
| dirres = np.cumsum(la.dsumsw2d(*lm, k, kpar, a, r, np.arange(800)))[[11, -1]] | ||
| assert isclose( | ||
| np.angle(la.lsumsw2d(*lm, k, kpar, a, r, 0) - dirres[0]), | ||
| np.angle(dirres[1] - dirres[0]), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize("lm", [(0, 0), (2, -2), (2, 0), (7, -7), (7, 3), (8, 0)]) | ||
| @pytest.mark.slow | ||
| def test_singular_r1(self, lm): | ||
| k = 1 | ||
| kpar = np.zeros(2) | ||
| a = np.array([[2 * np.pi, 0], [0, 1]]) | ||
| r = np.array([0.2, 0]) | ||
| dirres = np.cumsum(la.dsumsw2d(*lm, k, kpar, a, r, np.arange(800)))[[11, -1]] | ||
| assert isclose( | ||
| np.angle(la.lsumsw2d(*lm, k, kpar, a, r, 0) - dirres[0]), | ||
| np.angle(dirres[1] - dirres[0]), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| class TestLSumSW3d: | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 1), | ||
| (7, -7), | ||
| (7, 0), | ||
| (7, 3), | ||
| (8, -8), | ||
| (8, 0), | ||
| (8, 4), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [np.zeros(3), np.array([0.1, 0.2, -0.3])]) | ||
| @pytest.mark.parametrize( | ||
| "a", | ||
| [ | ||
| np.array([[1, 0, 0], [0, 2.0, 0], [0, 0, 3]]), | ||
| np.array([[1.3, 0.2, 0.1], [-0.1, 1.9, 0.2], [-0.1, 0.3, 2.2]]), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("r", [np.zeros(3), np.array([0.2, -0.3, 0.4])]) | ||
| @pytest.mark.slow | ||
| def test_regular(self, lm, k, kpar, a, r): | ||
| if lm[0] in (0, 2) and lm[1] == 0: | ||
| rtol = 0.2 | ||
| if ( | ||
| lm[0] == 2 | ||
| and np.array_equal(kpar, [0.1, 0.2, -0.3]) | ||
| and np.array_equal( | ||
| a, np.array([[1.3, 0.2, 0.1], [-0.1, 1.9, 0.2], [-0.1, 0.3, 2.2]]) | ||
| ) | ||
| ): | ||
| rtol = 0.3 | ||
| else: | ||
| rtol = 0.1 | ||
| assert isclose( | ||
| la.lsumsw3d(*lm, k, kpar, a, r, 0), | ||
| osscilation_avg(la.dsumsw3d(*lm, k, kpar, a, r, np.arange(120))), | ||
| rel_tol=rtol, | ||
| abs_tol=1e-8, | ||
| ) | ||
| # Only the imaginary part fits | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 1), | ||
| (5, -5), | ||
| (5, 0), | ||
| (5, 2), | ||
| (6, -6), | ||
| (6, 0), | ||
| (6, 3), | ||
| ], | ||
| ) | ||
| def test_singular_r0(self, lm): | ||
| k = 1 | ||
| kpar = np.zeros(3) | ||
| a = np.array([[2 * np.pi, 0, 0], [0, 2 * np.pi, 0], [0, 0, 3]]) | ||
| r = np.zeros(3) | ||
| assert isclose( | ||
| np.imag(la.lsumsw3d(*lm, k, kpar, a, r, 0)), | ||
| np.imag(np.sum(la.dsumsw3d(*lm, k, kpar, a, r, np.arange(2)))), | ||
| rel_tol=0.2, | ||
| abs_tol=1e-8, | ||
| ) | ||
| # Only the imaginary part fits | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 0), | ||
| (2, 1), | ||
| (5, -5), | ||
| (5, 0), | ||
| (5, 2), | ||
| (6, -6), | ||
| (6, 0), | ||
| (6, 3), | ||
| ], | ||
| ) | ||
| def test_singular_r1(self, lm): | ||
| k = 1 | ||
| kpar = np.zeros(3) | ||
| a = np.array([[2 * np.pi, 0, 0], [0, 2 * np.pi, 0], [0, 0, 3]]) | ||
| r = np.array([0.2, -0.3, 0.4]) | ||
| assert isclose( | ||
| np.imag(la.lsumsw3d(*lm, k, kpar, a, r, 0)), | ||
| np.imag(np.sum(la.dsumsw3d(*lm, k, kpar, a, r, np.arange(2)))), | ||
| rel_tol=0.2, | ||
| abs_tol=1e-8, | ||
| ) | ||
| class TestLSumSW2dShift: | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 0), | ||
| (2, 1), | ||
| (7, -7), | ||
| (7, 0), | ||
| (7, 3), | ||
| (8, -8), | ||
| (8, 0), | ||
| (8, 4), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [np.zeros(2), np.array([0.1, 0.2])]) | ||
| @pytest.mark.parametrize( | ||
| "a", [np.array([[1, 0], [0, 1.2]]), np.array([[1.1, 0.2], [-0.1, 1]])] | ||
| ) | ||
| @pytest.mark.parametrize("r", [np.array([0, 0, 0.1]), np.array([0.2, 0.2, 1.1])]) | ||
| @pytest.mark.slow | ||
| def test_regular(self, lm, k, kpar, a, r): | ||
| assert isclose( | ||
| la.lsumsw2d_shift(*lm, k, kpar, a, r, 0), | ||
| osscilation_avg(la.dsumsw2d_shift(*lm, k, kpar, a, r, np.arange(800))), | ||
| rel_tol=5e-2, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [(0, 0), (1, 0), (2, -2), (2, 0), (2, 1), (7, 0), (8, -8), (8, 0), (8, 4)], | ||
| ) | ||
| @pytest.mark.slow | ||
| def test_singular_r0(self, lm): | ||
| r = np.array([0, 0, 0.1]) | ||
| k = 1 | ||
| kpar = np.zeros(2) | ||
| a = np.array([[2 * np.pi, 0], [0, 1]]) | ||
| if (lm[0] + lm[1]) % 2 == 1: | ||
| assert isclose( | ||
| la.lsumsw2d_shift(*lm, k, kpar, a, r, 0), | ||
| la.dsumsw2d_shift(*lm, k, kpar, a, r, np.arange(800)).sum(), | ||
| rel_tol=1e-2, | ||
| abs_tol=EPS, | ||
| ) | ||
| else: | ||
| dirres = np.cumsum(la.dsumsw2d_shift(*lm, k, kpar, a, r, np.arange(800)))[ | ||
| [1, -1] | ||
| ] | ||
| assert isclose( | ||
| np.angle(la.lsumsw2d_shift(*lm, k, kpar, a, r, 0) - dirres[0]), | ||
| np.angle(dirres[1] - dirres[0]), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 0), | ||
| (2, 1), | ||
| (7, -7), | ||
| (7, 0), | ||
| (7, 3), | ||
| (8, -8), | ||
| (8, 0), | ||
| (8, 4), | ||
| ], | ||
| ) | ||
| @pytest.mark.slow | ||
| def test_singular_r1(self, lm): | ||
| r = np.array([0.2, 0.2, 1.1]) | ||
| k = 1 | ||
| kpar = np.zeros(2) | ||
| a = np.array([[2 * np.pi, 0], [0, 1]]) | ||
| if (lm[0] + lm[1]) % 2 == 1: | ||
| assert isclose( | ||
| la.lsumsw2d_shift(*lm, k, kpar, a, r, 0), | ||
| la.dsumsw2d_shift(*lm, k, kpar, a, r, np.arange(1000)).sum(), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| else: | ||
| dirres = np.cumsum(la.dsumsw2d_shift(*lm, k, kpar, a, r, np.arange(1000)))[ | ||
| [11, -1] | ||
| ] | ||
| assert isclose( | ||
| np.angle(la.lsumsw2d_shift(*lm, k, kpar, a, r, 0) - dirres[0]), | ||
| np.angle(dirres[1] - dirres[0]), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| class TestLSumSW1dShift: | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 0), | ||
| (2, 1), | ||
| (7, -7), | ||
| (7, 0), | ||
| (7, 3), | ||
| (8, -8), | ||
| (8, 0), | ||
| (8, 4), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [-0.2, 0, 0.7]) | ||
| @pytest.mark.parametrize("a", [1, 1.1]) | ||
| @pytest.mark.parametrize("r", [np.array([0, 0.1, 0]), np.array([0.2, 1.1, 0.2])]) | ||
| def test_regular(self, lm, k, kpar, a, r): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(*lm, k, kpar, a, r, 0), | ||
| la.dsumsw1d_shift(*lm, k, kpar, a, r, np.arange(100_000)).sum(), | ||
| rel_tol=1e-2, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize( | ||
| "lm", | ||
| [ | ||
| (0, 0), | ||
| (1, -1), | ||
| (1, 0), | ||
| (2, -2), | ||
| (2, 0), | ||
| (2, 1), | ||
| (7, -7), | ||
| (7, 0), | ||
| (7, 3), | ||
| (8, -8), | ||
| (8, 0), | ||
| (8, 4), | ||
| ], | ||
| ) | ||
| @pytest.mark.parametrize("r", [np.array([0, 0.1, 0]), np.array([0.2, 0.2, 1.1])]) | ||
| @pytest.mark.slow | ||
| def test_singular(self, lm, r): | ||
| k = 1 | ||
| kpar = 0 | ||
| a = 2 * np.pi | ||
| if lm == (2, 0) and np.array_equal(r, [0.2, 0.2, 1.1]): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(*lm, k, kpar, a, r, 0), | ||
| la.dsumsw1d_shift(*lm, k, kpar, a, r, np.arange(700_000)).sum(), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| else: | ||
| assert isclose( | ||
| la.lsumsw1d_shift(*lm, k, kpar, a, r, 0), | ||
| la.dsumsw1d_shift(*lm, k, kpar, a, r, np.arange(1_100_000)).sum(), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) | ||
| class TestLSumCW1dShift: | ||
| @pytest.mark.parametrize("l", [-2, -1, 0, 7, 8]) | ||
| @pytest.mark.parametrize("k", [2, 1.1 + 0.1j]) | ||
| @pytest.mark.parametrize("kpar", [-0.2, 0, 0.7]) | ||
| @pytest.mark.parametrize("a", [1, 1.1]) | ||
| @pytest.mark.parametrize("r", [np.array([0, 0.1]), np.array([0.2, 1.1])]) | ||
| def test_regular(self, l, k, kpar, a, r): # noqa: E741 | ||
| assert isclose( | ||
| la.lsumcw1d_shift(l, k, kpar, a, r, 0), | ||
| la.dsumcw1d_shift(l, k, kpar, a, r, np.arange(100_000)).sum(), | ||
| rel_tol=5e-2, | ||
| abs_tol=EPS, | ||
| ) | ||
| @pytest.mark.parametrize("l", [-2, -1, 0, 7, 8]) | ||
| @pytest.mark.parametrize("r", [np.array([0, 0.1]), np.array([0.2, 1.1])]) | ||
| @pytest.mark.slow | ||
| def test_singular(self, l, r): # noqa: E741 | ||
| k = 1 | ||
| kpar = 0 | ||
| a = 2 * np.pi | ||
| assert isclose( | ||
| la.lsumcw1d_shift(l, k, kpar, a, r, 0), | ||
| la.dsumcw1d_shift(l, k, kpar, a, r, np.arange(1_800_000)).sum(), | ||
| rel_tol=1e-1, | ||
| abs_tol=EPS, | ||
| ) |
| import numpy as np | ||
| import treams.coeffs as cf | ||
| EPS = 2e-7 | ||
| EPSSQ = 4e-14 | ||
| class TestMie: | ||
| def test_real(self): | ||
| expect = np.array( | ||
| [ | ||
| [ | ||
| -0.207488775205865 - 0.398855023857451j, | ||
| 0.047334571416857 + 0.044794841521540j, | ||
| ], | ||
| [ | ||
| 0.059646659631622 + 0.056446326342700j, | ||
| -0.117865783454384 - 0.314040741356843j, | ||
| ], | ||
| ] | ||
| ) | ||
| assert np.all( | ||
| np.abs( | ||
| cf.mie(1, [1, 2, 4], [1, 1.3, 3, 2], [1, 2, 1, 1.5], [0.1, 0, 0.2, 0.1]) | ||
| - expect | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| def test_complex(self): | ||
| expect = np.array( | ||
| [ | ||
| [ | ||
| -0.233064414843298 + 0.218720052849983j, | ||
| 0.047852376140501 + 0.048918498716278j, | ||
| ], | ||
| [ | ||
| 0.084573700327288 + 0.086457952239276j, | ||
| -0.619693739232935 + 0.081746714390217j, | ||
| ], | ||
| ] | ||
| ) | ||
| assert np.all( | ||
| np.abs( | ||
| cf.mie( | ||
| 3, | ||
| [1, 2, 3], | ||
| [1, 2, 3 + 1j, 4], | ||
| [4, 3 + 0.1j, 1, 2], | ||
| [0.3, -0.1 + 0.1j, 1 + 0.1j, 0.4], | ||
| ) | ||
| - expect | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestFresnel: | ||
| def test_real(self): | ||
| expect = [ | ||
| [ | ||
| [[1.042449234640745, 0], [0, 1.005929062176551]], | ||
| [[-0.042449234640745, 0], [0, -0.005929062176551]], | ||
| ], | ||
| [ | ||
| [[0.042449234640745, 0], [0, 0.005929062176551]], | ||
| [[0.957550765359255, 0], [0, 0.994070937823449]], | ||
| ], | ||
| ] | ||
| assert np.all( | ||
| np.abs( | ||
| cf.fresnel( | ||
| [[3, 5], [2, 4]], | ||
| [[np.sqrt(8), np.sqrt(24)], [np.sqrt(3), np.sqrt(15)]], | ||
| [1, 1], | ||
| ) | ||
| - expect | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| def test_evanescent(self): | ||
| expect = [ | ||
| [ | ||
| [ | ||
| [1.6, 0], | ||
| [-0.3525 - 0.892941067484299j, 1.0125 - 1.488235112473832j], | ||
| ], | ||
| [[0, 0.6], [0.6, 0.235 + 0.595294044989533j]], | ||
| ], | ||
| [ | ||
| [ | ||
| [0.1321875 + 0.334852900306612j, -0.3796875 + 0.558088167177687j], | ||
| [-0.8203125 - 0.558088167177687j, -0.3671875 - 0.930146945296145j], | ||
| ], | ||
| [ | ||
| [0.4, -0.088125 - 0.223235266871075j], | ||
| [0, 0.546875 + 0.372058778118458j], | ||
| ], | ||
| ], | ||
| ] | ||
| assert np.all( | ||
| np.abs( | ||
| cf.fresnel( | ||
| [[3, 5], [3, 3]], | ||
| [[1j * np.sqrt(7), 3], [1j * np.sqrt(7), 1j * np.sqrt(7)]], | ||
| [1 / 4, 1], | ||
| ) | ||
| - expect | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| def test_complex(self): | ||
| expect = [ | ||
| [ | ||
| [ | ||
| [ | ||
| 1.097225776644201 + 0.417220660903015j, | ||
| -0.091009961428571 + 0.126135520878490j, | ||
| ], | ||
| [ | ||
| -0.001231951759500 + 0.002873123884810j, | ||
| 1.344486739106106 + 0.035486557500712j, | ||
| ], | ||
| ], | ||
| [ | ||
| [ | ||
| 0.150107671767365 - 0.265167179806910j, | ||
| 0.338343409840136 + 0.025917960217615j, | ||
| ], | ||
| [ | ||
| 0.341187323894434 + 0.025964969567879j, | ||
| -0.004531366971172 - 0.006648464048023j, | ||
| ], | ||
| ], | ||
| ], | ||
| [ | ||
| [ | ||
| [ | ||
| -0.174619475651097 + 0.295135397879729j, | ||
| -0.406453529679773 + 0.067329209352982j, | ||
| ], | ||
| [ | ||
| -0.273077204054797 - 0.119212139138476j, | ||
| 0.029043170854903 - 0.023319754024796j, | ||
| ], | ||
| ], | ||
| [ | ||
| [ | ||
| 0.768675971525422 - 0.225752788034603j, | ||
| 0.000510025554099 + 0.002053400492144j, | ||
| ], | ||
| [ | ||
| -0.040244376347509 + 0.065379361340185j, | ||
| 0.657635248742791 - 0.030513023773495j, | ||
| ], | ||
| ], | ||
| ], | ||
| ] | ||
| epsilon = np.array([4 + 1j, 3 + 0.1j]) | ||
| mu = np.array([1 + 0.1j, 3 + 0.01j]) | ||
| kappa = np.array([1 + 0.1j, -0.1]) | ||
| ks = np.stack( | ||
| (np.sqrt(epsilon * mu) - kappa, np.sqrt(epsilon * mu) + kappa), axis=-1 | ||
| ) | ||
| kzs = np.sqrt(ks * ks - 1) | ||
| zs = np.sqrt(mu / epsilon) | ||
| assert np.all(np.abs(cf.fresnel(ks, kzs, zs) - expect) < EPSSQ) | ||
| class TestMieCyl: | ||
| def test_real(self): | ||
| # Calculated with comsol | ||
| expect = [ | ||
| [-0.87100 - 0.069690j, -0.14510 - 0.2492j], | ||
| [-0.18286 - 0.30517j, -0.62281 + 0.37137j], | ||
| ] | ||
| m = -2 | ||
| epsilon = [1, 4, 2] | ||
| mu = [4, 3, 2] | ||
| kappa = [-0.5, 0.1, 0.2] | ||
| kz = 2 * np.pi / 600 | ||
| k0 = 2 * np.pi / 500 | ||
| radii = [50, 100] | ||
| assert np.all( | ||
| np.abs(cf.mie_cyl(kz, m, k0, radii, epsilon, mu, kappa) - expect) | ||
| / np.max(np.abs(expect)) | ||
| < 1.1e-2 | ||
| ) | ||
| def test_evanescent(self): | ||
| # Calculated with comsol | ||
| expect = [ | ||
| [-3.2138e-4 - 29.992j, -0.0031377 + 43.380j], | ||
| [6.7389e-4 + 35.637j, -0.0090938 - 74.594j], | ||
| ] | ||
| m = 1 | ||
| epsilon = [3, 2, 2] | ||
| mu = [6, 1, 2] | ||
| kappa = [1, 0, -0.2] | ||
| kz = 2 * np.pi / 150 | ||
| k0 = 2 * np.pi / 500 | ||
| radii = [30, 100] | ||
| assert np.all( | ||
| np.abs(cf.mie_cyl(kz, m, k0, radii, epsilon, mu, kappa) - expect) | ||
| / np.max(np.abs(expect)) | ||
| < 1e-2 | ||
| ) | ||
| def test_complex(self): | ||
| # Calculated with comsol | ||
| expect = [ | ||
| [-0.88746 - 0.15313j, 0.12955 + 0.086434j], | ||
| [0.103112 + 0.081574j, -0.098055 + 0.017689j], | ||
| ] | ||
| m = -1 | ||
| epsilon = [4, 2 + 1j, 2 + 0.02j] | ||
| mu = [1 + 0.5j, 1, 2 + 0.1j] | ||
| kappa = [-0.5, 0.1 + 0.1j, -0.2 + 0.001j] | ||
| kz = 2 * np.pi / 250 | ||
| k0 = 2 * np.pi / 500 | ||
| radii = [50, 100] | ||
| assert np.all( | ||
| np.abs(cf.mie_cyl(kz, m, k0, radii, epsilon, mu, kappa) - expect) | ||
| / np.max(np.abs(expect)) | ||
| < 1e-2 | ||
| ) |
| import numpy as np | ||
| import pytest | ||
| import treams | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestSWB: | ||
| def test_init_empty(self): | ||
| b = treams.SphericalWaveBasis([]) | ||
| assert b.l.size == 0 and b.m.size == 0 and b.pol.size == 0 and b.pidx.size == 0 | ||
| def test_init_numpy(self): | ||
| b = treams.SphericalWaveBasis(np.array([[1, 0, 0]]), [0, 1, 0]) | ||
| assert ( | ||
| np.all(b.l == [1]) | ||
| and np.all(b.m == [0]) | ||
| and np.all(b.pol == [0]) | ||
| and np.all(b.pidx == [0]) | ||
| ) | ||
| def test_init_duplicate(self): | ||
| b = treams.SphericalWaveBasis([[0, 1, 0, 0], [0, 1, 0, 0]]) | ||
| assert ( | ||
| np.all(b.l == [1]) | ||
| and np.all(b.m == [0]) | ||
| and np.all(b.pol == [0]) | ||
| and np.all(b.pidx == [0]) | ||
| ) | ||
| def test_init_invalid_shape(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[0, 0], [0, 0]]) | ||
| def test_init_invalid_positions(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[1, 0, 0]], [1, 2]) | ||
| def test_init_non_int_value(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[1.1, 0, 0]]) | ||
| def test_init_non_natural_l(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[0, 0, 0]]) | ||
| def test_init_non_too_large_m(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[1, -2, 0]]) | ||
| def test_init_invalid_pol(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[1, 0, 2]]) | ||
| def test_init_unsecified_positions(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis([[1, 1, 0, 0]]) | ||
| def test_property_positions(self): | ||
| a = np.array([[1, 2, 3]]) | ||
| b = treams.SphericalWaveBasis([[1, 0, 0]], a) | ||
| assert (a == b.positions).all() | ||
| def test_repr(self): | ||
| b = treams.SphericalWaveBasis( | ||
| [[0, 1, 0, 0], [1, 1, 0, 0]], [[0.0, 0, 0], [1, 0, 0]] | ||
| ) | ||
| assert ( | ||
| repr(b) | ||
| == """SphericalWaveBasis( | ||
| pidx=[0 1], | ||
| l=[1 1], | ||
| m=[0 0], | ||
| pol=[0 0], | ||
| positions=[[0. 0. 0.], [1. 0. 0.]], | ||
| )""" | ||
| ) | ||
| def test_getitem_plms(self): | ||
| b = treams.SphericalWaveBasis([[0, 2, -1, 1]]) | ||
| assert b.plms == ([0], [2], [-1], [1]) | ||
| def test_getitem_lms(self): | ||
| b = treams.SphericalWaveBasis([[2, -1, 1]]) | ||
| assert b.lms == ([2], [-1], [1]) | ||
| def test_getitem_invalid_index(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| with pytest.raises(AttributeError): | ||
| b.fail | ||
| def test_getitem_int(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert b[1] == (0, 1, 0, 1) | ||
| def test_getitem_tuple(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert (np.array(b[()]) == ([0, 0], [1, 1], [0, 0], [0, 1])).all() | ||
| def test_getitem_slice(self): | ||
| a = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert a == b[:1] | ||
| def test_default(self): | ||
| a = treams.SphericalWaveBasis.default(2, 2, [[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis( | ||
| zip( | ||
| 16 * [0] + 16 * [1], | ||
| 2 * (6 * [1] + 10 * [2]), | ||
| 2 * [-1, -1, 0, 0, 1, 1, -2, -2, -1, -1, 0, 0, 1, 1, 2, 2], | ||
| 16 * [1, 0], | ||
| ), | ||
| [[0, 0, 0], [1, 0, 0]], | ||
| ) | ||
| assert a == b | ||
| def test_ebcm(self): | ||
| a = treams.SphericalWaveBasis.ebcm(2, 2, positions=[[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis( | ||
| zip( | ||
| 16 * [0] + 16 * [1], | ||
| 2 * ([2, 2] + 3 * [1, 1, 2, 2] + [2, 2]), | ||
| 2 * [-2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2], | ||
| 16 * [1, 0], | ||
| ), | ||
| [[0, 0, 0], [1, 0, 0]], | ||
| ) | ||
| assert a == b | ||
| def test_ebcm_mmax(self): | ||
| a = treams.SphericalWaveBasis.ebcm(2, mmax=1) | ||
| b = treams.SphericalWaveBasis( | ||
| zip( | ||
| 3 * [1, 1, 2, 2], [-1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1], 6 * [1, 0], | ||
| ), | ||
| ) | ||
| assert a == b | ||
| def test_defaultlmax(self): | ||
| assert treams.SphericalWaveBasis.defaultlmax(60, 2) == 3 | ||
| def test_defaultlmax_fail(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis.defaultlmax(1) | ||
| def test_defaultdim(self): | ||
| assert treams.SphericalWaveBasis.defaultdim(3, 2) == 60 | ||
| def test_defaultdim_fail(self): | ||
| with pytest.raises(ValueError): | ||
| treams.SphericalWaveBasis.defaultdim(1, -1) | ||
| def test_property_isglobal_true(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert b.isglobal | ||
| def test_property_isglobal_false(self): | ||
| b = treams.SphericalWaveBasis( | ||
| [[0, 1, 0, 0], [1, 1, 0, 0]], [[0, 0, 0], [1, 0, 0]] | ||
| ) | ||
| assert not b.isglobal | ||
| def test_from_iterable(self): | ||
| a = treams.SphericalWaveBasis.default(1) | ||
| b = treams.SphericalWaveBasis.default(2) | ||
| assert a & b == a | ||
| def test_neq(self): | ||
| b = treams.SphericalWaveBasis.default(1) | ||
| assert not b == [] | ||
| class TestCWB: | ||
| def test_init_empty(self): | ||
| b = treams.CylindricalWaveBasis([]) | ||
| assert b.kz.size == 0 and b.m.size == 0 and b.pol.size == 0 and b.pidx.size == 0 | ||
| def test_init_numpy(self): | ||
| b = treams.CylindricalWaveBasis(np.array([[0.5, 0, 0]]), [0, 1, 0]) | ||
| assert ( | ||
| np.all(b.kz == [0.5]) | ||
| and np.all(b.m == [0]) | ||
| and np.all(b.pol == [0]) | ||
| and np.all(b.pidx == [0]) | ||
| ) | ||
| def test_init_duplicate(self): | ||
| b = treams.CylindricalWaveBasis([[0, 1, 0, 0], [0, 1, 0, 0]]) | ||
| assert ( | ||
| np.all(b.kz == [1]) | ||
| and np.all(b.m == [0]) | ||
| and np.all(b.pol == [0]) | ||
| and np.all(b.pidx == [0]) | ||
| ) | ||
| def test_init_invalid_shape(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis([[0, 0], [0, 0]]) | ||
| def test_init_invalid_positions(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis([[1, 0, 0]], [1, 2]) | ||
| def test_init_non_int_value(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis([[1, 0.2, 0]]) | ||
| def test_init_invalid_pol(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis([[1, 0, 2]]) | ||
| def test_init_unsecified_positions(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis([[1, 1, 0, 0]]) | ||
| def test_property_positions(self): | ||
| a = np.array([[1, 2, 3]]) | ||
| b = treams.CylindricalWaveBasis([[1, 0, 0]], a) | ||
| assert (a == b.positions).all() | ||
| def test_repr(self): | ||
| b = treams.CylindricalWaveBasis( | ||
| [[0, 1, 0, 0], [1, 1, 0, 0]], [[0.0, 0, 0], [1, 0, 0]] | ||
| ) | ||
| assert ( | ||
| repr(b) | ||
| == """CylindricalWaveBasis( | ||
| pidx=[0 1], | ||
| kz=[1. 1.], | ||
| m=[0 0], | ||
| pol=[0 0], | ||
| positions=[[0. 0. 0.], [1. 0. 0.]], | ||
| )""" | ||
| ) | ||
| def test_getitem_pzms(self): | ||
| b = treams.CylindricalWaveBasis([[0, 2, -1, 1]]) | ||
| assert b.pzms == ([0], [2], [-1], [1]) | ||
| def test_getitem_zms(self): | ||
| b = treams.CylindricalWaveBasis([[2, -1, 1]]) | ||
| assert b.zms == ([2], [-1], [1]) | ||
| def test_getitem_invalid_index(self): | ||
| b = treams.CylindricalWaveBasis([[1, 0, 0]]) | ||
| with pytest.raises(AttributeError): | ||
| b.fail | ||
| def test_getitem_int(self): | ||
| b = treams.CylindricalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert b[1] == (0, 1, 0, 1) | ||
| def test_getitem_tuple(self): | ||
| b = treams.CylindricalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert (np.array(b[()]) == ([0, 0], [1, 1], [0, 0], [0, 1])).all() | ||
| def test_getitem_slice(self): | ||
| a = treams.CylindricalWaveBasis([[1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert a == b[:1] | ||
| def test_default(self): | ||
| a = treams.CylindricalWaveBasis.default( | ||
| [0.3, -0.2], 2, 2, [[0, 0, 0], [1, 0, 0]] | ||
| ) | ||
| b = treams.CylindricalWaveBasis( | ||
| zip( | ||
| 20 * [0] + 20 * [1], | ||
| 2 * (10 * [0.3] + 10 * [-0.2]), | ||
| 4 * [-2, -2, -1, -1, 0, 0, 1, 1, 2, 2], | ||
| 20 * [1, 0], | ||
| ), | ||
| [[0, 0, 0], [1, 0, 0]], | ||
| ) | ||
| assert a == b | ||
| def test_defaultmmax(self): | ||
| assert treams.CylindricalWaveBasis.defaultmmax(112, 4, 2) == 3 | ||
| def test_defaultmmax_fail(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis.defaultmmax(1) | ||
| def test_defaultdim(self): | ||
| assert treams.CylindricalWaveBasis.defaultdim(3, 2, 4) == 120 | ||
| def test_defaultdim_fail(self): | ||
| with pytest.raises(ValueError): | ||
| treams.CylindricalWaveBasis.defaultdim(1, -1) | ||
| def test_property_isglobal_true(self): | ||
| b = treams.CylindricalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| assert b.isglobal | ||
| def test_property_isglobal_false(self): | ||
| b = treams.CylindricalWaveBasis( | ||
| [[0, 1, 0, 0], [1, 1, 0, 0]], [[0, 0, 0], [1, 0, 0]] | ||
| ) | ||
| assert not b.isglobal | ||
| def test_from_iterable(self): | ||
| a = treams.CylindricalWaveBasis.default(0, 1) | ||
| b = treams.CylindricalWaveBasis.default(0, 2) | ||
| assert a & b == a | ||
| def test_neq(self): | ||
| b = treams.CylindricalWaveBasis.default(0, 1) | ||
| assert not b == [] | ||
| def test_diffr_orders(self): | ||
| a = treams.CylindricalWaveBasis.diffr_orders(0.1, 1, 2 * np.pi, 1.5) | ||
| b = treams.CylindricalWaveBasis( | ||
| zip( | ||
| *[ | ||
| 6 * [-0.9] + 6 * [0.1] + 6 * [1.1], | ||
| 3 * [-1, -1, 0, 0, 1, 1], | ||
| 18 * [1, 0], | ||
| ] | ||
| ) | ||
| ) | ||
| assert ( | ||
| a == b | ||
| and a.lattice == treams.Lattice(2 * np.pi) | ||
| and a.kpar == [np.nan, np.nan, 0.1] | ||
| ) | ||
| class TestPWBUV: | ||
| def test_init_empty(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([]) | ||
| assert b.qx.size == 0 and b.qy.size == 0 and b.qz.size == 0 and b.pol.size == 0 | ||
| def test_init_numpy(self): | ||
| b = treams.PlaneWaveBasisByUnitVector(np.array([[0.1, 0.2, 0.2, 0]])) | ||
| assert ( | ||
| np.all(b.qx == [1 / 3]) | ||
| and np.all(b.qy == [2 / 3]) | ||
| and np.all(b.qz == [2 / 3]) | ||
| and np.all(b.pol == [0]) | ||
| ) | ||
| def test_init_duplicate(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[0.1, 0.2, 0.2, 0], [0.1, 0.2, 0.2, 0]]) | ||
| assert ( | ||
| np.all(b.qx == [1 / 3]) | ||
| and np.all(b.qy == [2 / 3]) | ||
| and np.all(b.qz == [2 / 3]) | ||
| and np.all(b.pol == [0]) | ||
| ) | ||
| def test_init_invalid_shape(self): | ||
| with pytest.raises(ValueError): | ||
| treams.PlaneWaveBasisByUnitVector([[0, 0], [0, 0]]) | ||
| def test_init_invalid_pol(self): | ||
| with pytest.raises(ValueError): | ||
| treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 2]]) | ||
| def test_repr(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[0.0, 1.0, 0.0, 0], [1, 0, 0, 0]]) | ||
| assert ( | ||
| repr(b) | ||
| == """PlaneWaveBasisByUnitVector( | ||
| qx=[0. 1.], | ||
| qy=[1. 0.], | ||
| qz=[0. 0.], | ||
| pol=[0 0], | ||
| )""" | ||
| ) | ||
| def test_getitem_xyzs(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[0, 4, -3, 1]]) | ||
| assert b.xyzs == ([0], [0.8], [-0.6], [1]) | ||
| def test_getitem_xys(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[0, 4, -3, 1]]) | ||
| assert b.xys == ([0], [0.8], [1]) | ||
| def test_getitem_invalid_index(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0]]) | ||
| with pytest.raises(AttributeError): | ||
| b.fail | ||
| def test_getitem_int(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [1, 0, 0, 1]]) | ||
| assert b[1] == (1, 0, 0, 1) | ||
| def test_getitem_tuple(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [1, 0, 0, 1]]) | ||
| assert (np.array(b[()]) == ([1, 1], [0, 0], [0, 0], [0, 1])).all() | ||
| def test_getitem_slice(self): | ||
| a = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0]]) | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [1, 0, 0, 1]]) | ||
| assert a == b[:1] | ||
| def test_default(self): | ||
| a = treams.PlaneWaveBasisByUnitVector.default([0.3, -0.2, 0.1]) | ||
| b = treams.PlaneWaveBasisByUnitVector( | ||
| [[0.3, -0.2, 0.1, 1], [0.3, -0.2, 0.1, 0]] | ||
| ) | ||
| assert a == b | ||
| def test_property_isglobal_true(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([]) | ||
| assert b.isglobal | ||
| def test_from_iterable(self): | ||
| a = treams.PlaneWaveBasisByUnitVector.default([0, 0, 1]) | ||
| b = treams.PlaneWaveBasisByUnitVector.default([[0, 0, 1], [0, 1, 0]]) | ||
| assert a & b == a | ||
| def test_bycomp(self): | ||
| a = treams.PlaneWaveBasisByComp.default([0, 1], "yz") | ||
| b = treams.PlaneWaveBasisByUnitVector.default([0, 0, 1]) | ||
| assert b.bycomp(1, "yz") == a | ||
| class TestPWBC: | ||
| def test_init_empty(self): | ||
| b = treams.PlaneWaveBasisByComp([]) | ||
| assert b.kx.size == 0 and b.ky.size == 0 and b.kz is None and b.pol.size == 0 | ||
| def test_init_numpy(self): | ||
| b = treams.PlaneWaveBasisByComp(np.array([[0.4, 0.2, 0]]), "zx") | ||
| assert ( | ||
| np.all(b.kz == [0.4]) | ||
| and np.all(b.kx == [0.2]) | ||
| and np.all(b.ky is None) | ||
| and np.all(b.pol == [0]) | ||
| ) | ||
| def test_init_duplicate(self): | ||
| b = treams.PlaneWaveBasisByComp([[0.4, 0.2, 0], [0.4, 0.2, 0]]) | ||
| assert ( | ||
| np.all(b.kx == [0.4]) | ||
| and np.all(b.ky == [0.2]) | ||
| and np.all(b.kz is None) | ||
| and np.all(b.pol == [0]) | ||
| ) | ||
| def test_init_invalid_shape(self): | ||
| with pytest.raises(ValueError): | ||
| treams.PlaneWaveBasisByComp([[0, 0], [0, 0]]) | ||
| def test_init_invalid_pol(self): | ||
| with pytest.raises(ValueError): | ||
| treams.PlaneWaveBasisByComp([[1, 0, 2]]) | ||
| def test_repr(self): | ||
| b = treams.PlaneWaveBasisByComp([[0.0, 1.0, 0], [1, 1, 0]], "yz") | ||
| assert ( | ||
| repr(b) | ||
| == """PlaneWaveBasisByComp( | ||
| ky=[0. 1.], | ||
| kz=[1. 1.], | ||
| pol=[0 0], | ||
| )""" | ||
| ) | ||
| def test_from_iterable(self): | ||
| a = treams.PlaneWaveBasisByComp.default([0, 0]) | ||
| b = treams.PlaneWaveBasisByComp.default([[0, 0], [0, 1]]) | ||
| assert a & b == a | ||
| def test_getitem_int(self): | ||
| b = treams.PlaneWaveBasisByComp([[1, 0, 0], [1, 0, 1]]) | ||
| assert b[1] == (1, 0, 1) | ||
| def test_getitem_tuple(self): | ||
| b = treams.PlaneWaveBasisByComp([[1, 0, 0], [1, 0, 1]]) | ||
| assert (np.array(b[()]) == ([1, 1], [0, 0], [0, 1])).all() | ||
| def test_getitem_slice(self): | ||
| a = treams.PlaneWaveBasisByComp([[1, 0, 0]]) | ||
| b = treams.PlaneWaveBasisByComp([[1, 0, 0], [1, 0, 0, 1]]) | ||
| assert a == b[:1] | ||
| def test_byunitvector(self): | ||
| a = treams.PlaneWaveBasisByUnitVector.default([0, 0, 1]) | ||
| b = treams.PlaneWaveBasisByComp.default([0, 1], "yz") | ||
| assert b.byunitvector(1) == a | ||
| def test_diffr_orders(self): | ||
| lattice = treams.Lattice(2 * np.pi * np.eye(2)) | ||
| b = treams.PlaneWaveBasisByComp.diffr_orders([0, 0], lattice, 1) | ||
| a = treams.PlaneWaveBasisByComp.default( | ||
| [[0, 0], [0, 1], [1, 0], [-1, 0], [0, -1]] | ||
| ) | ||
| assert a <= b and b <= a and b.lattice == lattice and b.kpar == [0, 0, np.nan] | ||
| class TestPhysicsArray: | ||
| def test_init(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray(np.eye(2), basis=b) | ||
| assert (p == np.eye(2)).all() and p.basis == b | ||
| def test_type_error(self): | ||
| with pytest.raises(TypeError): | ||
| treams.PhysicsArray(np.eye(2), basis="fail") | ||
| def test_lattice(self): | ||
| b = treams.PlaneWaveBasisByComp.diffr_orders([0, 0], np.eye(2), 4) | ||
| p = treams.PhysicsArray([1, 2], lattice=treams.Lattice(1, "x"), basis=b) | ||
| assert p.lattice == treams.Lattice(1, "x") | ||
| def test_matmul(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([[1, 1], [2, 2]], basis=b) | ||
| x = p @ [1, 2] | ||
| assert (x == [3, 6]).all() and x.basis == b | ||
| def test_rmatmul(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([[1, 1], [2, 2]], basis=b) | ||
| x = [1, 2] @ p | ||
| assert (x == [5, 5]).all() and x.basis == b | ||
| def test_changepoltype(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([[1, 0], [0, 1]], basis=b, poltype="helicity") | ||
| x = p.changepoltype.apply_left() | ||
| assert ( | ||
| x == np.sqrt(0.5) * np.array([[-1, 1], [1, 1]]) | ||
| ).all() and x.poltype == ("parity", "helicity",) | ||
| def test_changepoltype_inv(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([[1, 0], [0, 1]], basis=b, poltype="helicity") | ||
| x = p.changepoltype.apply_right() | ||
| assert ( | ||
| x == np.sqrt(0.5) * np.array([[-1, 1], [1, 1]]) | ||
| ).all() and x.poltype == ("helicity", "parity",) | ||
| def test_efield(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b, k0=1, poltype="helicity") | ||
| x = p.efield([1, 0, 0]) | ||
| assert ( | ||
| np.abs( | ||
| x | ||
| - ( | ||
| treams.special.vsph2car( | ||
| treams.special.vsw_rA(1, 0, 1, np.pi / 2, 0, [0, 1]), | ||
| [1, np.pi / 2, 0], | ||
| ).sum(0) | ||
| ) | ||
| < 1e-15 | ||
| ) | ||
| ).all() | ||
| def test_efield_inv(self): | ||
| with pytest.raises(NotImplementedError): | ||
| treams.PhysicsArray([0]).efield.apply_right([0, 0, 0]) | ||
| def test_expand(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b, k0=1, poltype="helicity") | ||
| x = p.expand(b) | ||
| assert (np.abs(x - [1, 0]) < 1e-14).all() | ||
| def test_expand_inv(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b, k0=1, poltype="helicity") | ||
| x = p.expand.eval_inv(b) | ||
| assert (np.abs(x - np.eye(2)) < 1e-14).all() | ||
| def test_expandlattice(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b, k0=1, poltype="helicity") | ||
| x = p.expandlattice.eval(lattice=[[1, 0], [0, 1]], kpar=[0, 0]) | ||
| assert x.modetype == ("regular", "singular") | ||
| def test_expandlattice_inv(self): | ||
| with pytest.raises(NotImplementedError): | ||
| treams.PhysicsArray([0]).expandlattice.eval_inv(lattice=1) | ||
| def test_permute(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [1, 0, 0, 1]]) | ||
| c = treams.PlaneWaveBasisByUnitVector([[0, 1, 0, 0], [0, 1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b) | ||
| assert p.permute.eval().basis == (c, b) | ||
| def test_permute_inv(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [1, 0, 0, 1]]) | ||
| c = treams.PlaneWaveBasisByUnitVector([[0, 1, 0, 0], [0, 1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b) | ||
| assert p.permute.eval_inv().basis == (b, c) | ||
| def test_rotate(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b) | ||
| x = p.rotate.eval(1, 2, 3) | ||
| y = np.diag([treams.special.wignerd(1, 0, 0, 1, 2, 3)] * 2) | ||
| assert (np.abs(x - y) < 1e-14).all() | ||
| def test_rotate_inv(self): | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 0, 0], [1, 1, 0]]) | ||
| p = treams.PhysicsArray([1, 0, 0], basis=b) | ||
| x = p.rotate.eval_inv(1, 2, 3) | ||
| y = treams.special.wignerd(1, [[-1], [0], [1]], [-1, 0, 1], 1, 2, 3) | ||
| assert (np.abs(x - y.conj().T) < 1e-14).all() | ||
| def test_translate(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b, k0=1) | ||
| x = p.translate.eval([0, 0, 0]) | ||
| assert (np.abs(x - np.eye(2)) < 1e-14).all() | ||
| def test_translate_inv(self): | ||
| b = treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]]) | ||
| p = treams.PhysicsArray([1, 0], basis=b, k0=1) | ||
| x = p.translate.eval_inv([0, 0, 0]) | ||
| assert (np.abs(x - np.eye(2)) < 1e-14).all() |
| import numpy as np | ||
| import scipy.special as sc | ||
| from treams import cw | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestPeriodicToPw: | ||
| def test(self): | ||
| assert isclose( | ||
| cw.periodic_to_pw(6, -5, 4, 1, 4, 3, 1, 2), | ||
| 2 * np.power((-5 - 6j) / np.sqrt(61), 3) / 10, | ||
| ) | ||
| def test_ky_zero(self): | ||
| assert isclose(cw.periodic_to_pw(6, 0, 4, 1, 4, 3, 1, 2), 5e19 + 5e19j) | ||
| def test_pol(self): | ||
| assert cw.periodic_to_pw(6, 5j, 4, 1, 4, 3, 0, 2) == 0 | ||
| class TestRotate: | ||
| def test(self): | ||
| assert isclose(cw.rotate(3, 2, 1, 3, 2, 1, 4), np.exp(-8j)) | ||
| def test_zero(self): | ||
| assert cw.rotate(3, 2, 1, 2, 2, 1, 4) == 0 | ||
| class TestToSw: | ||
| def test_h(self): | ||
| assert isclose(cw.to_sw(4, 3, 1, 2, 3, 1, 5), -2.481668635232552j) | ||
| def test_h_zero(self): | ||
| assert cw.to_sw(4, 3, 1, 2, 2, 1, 5) == 0 | ||
| def test_h_pol(self): | ||
| assert cw.to_sw(4, 3, 1, 2, 3, 0, 5) == 0 | ||
| def test_p_same(self): | ||
| assert isclose( | ||
| cw.to_sw(4, 3, 1, 2, 3, 1, 5, poltype="parity"), 1.063572272242522j | ||
| ) | ||
| def test_p_same_zero(self): | ||
| assert cw.to_sw(4, 3, 1, 2, 2, 1, 5, poltype="parity") == 0 | ||
| def test_p_opposite(self): | ||
| assert isclose( | ||
| cw.to_sw(4, 3, 1, 2, 3, 0, 5, poltype="parity"), -3.545240907475074j | ||
| ) | ||
| def test_p_opposite_zero(self): | ||
| assert cw.to_sw(4, 3, 1, 2, 2, 0, 5, poltype="parity") == 0 | ||
| class TestTranslate: | ||
| def test_s(self): | ||
| assert isclose( | ||
| cw.translate(3, 2, 1, 3, -2, 1, 4, 5, 6), sc.hankel1(-4, 4) * np.exp(-2j) | ||
| ) | ||
| def test_s_opposite(self): | ||
| assert cw.translate(3, 2, 0, 0, 2, 1, 4 + 1j, 5, 6) == 0 | ||
| def test_s_zero(self): | ||
| assert cw.translate(3, 2, 0, 0, 2, 1, 0, 5, 0) == 0 | ||
| def test_r(self): | ||
| assert isclose( | ||
| cw.translate(3, 2, 1, 3, -2, 1, 4, 5, 6, singular=False), | ||
| sc.jv(-4, 4) * np.exp(-2j), | ||
| ) | ||
| def test_r_opposite(self): | ||
| assert cw.translate(3, 2, 0, 0, 2, 1, 4 + 1j, 5, 6, singular=False) == 0 | ||
| class TestTranslatePeriodic: | ||
| def test(self): | ||
| assert isclose( | ||
| cw.translate_periodic(1, 0, 2, [0, 0, 0], ([0], [0], [0]))[0, 0], | ||
| 0.76102358j, | ||
| ) | ||
| def test_ks(self): | ||
| assert isclose( | ||
| cw.translate_periodic([1, 2], 0, 2, [0, 0, 0], ([1], [3], [1]))[0, 0], | ||
| -0.42264973081037416 + 0.30599871466756257j, | ||
| ) | ||
| def test_ks_same(self): | ||
| assert isclose( | ||
| cw.translate_periodic( | ||
| [1, 1], [0, 1], [[2, 0], [0, 2]], [0, 0, 0], ([0], [0], [0]) | ||
| )[0, 0], | ||
| -1.0 + 0.28364898j, | ||
| ) |
| import os | ||
| import tempfile | ||
| import h5py | ||
| import numpy as np | ||
| import pytest | ||
| import treams | ||
| from treams import io | ||
| @pytest.mark.gmsh | ||
| def test_meshspheres(): | ||
| import gmsh | ||
| gmsh.initialize() | ||
| gmsh.model.add("spheres") | ||
| io.mesh_spheres([1, 2], [[0, 0, 2], [0, 0, -2]], gmsh.model) | ||
| with tempfile.TemporaryDirectory() as directory: | ||
| filename = os.path.join(directory, "spheres.msh") | ||
| gmsh.write(filename) | ||
| gmsh.finalize() | ||
| with open(filename, "r") as f: | ||
| value = "".join([line for line, _ in zip(f, range(5))][3:]) | ||
| expect = "$Entities\n4 6 2 2\n" | ||
| assert value == expect | ||
| class TestSaveHDF5: | ||
| def test_helicity(self): | ||
| with h5py.File("test.h5", "x", driver="core", backing_store=False) as fp: | ||
| m = np.arange(4 * 3 * 16 * 16).reshape((4, 3, 16, 16)) | ||
| tms = [ | ||
| [ | ||
| treams.TMatrix( | ||
| m[i, ii], k0=7, material=treams.Material(i + 1, ii + 1, 0.5) | ||
| ) | ||
| for ii in range(3) | ||
| ] | ||
| for i in range(4) | ||
| ] | ||
| io.save_hdf5( | ||
| fp, | ||
| tms, | ||
| "testname", | ||
| "testdescr", | ||
| lunit="µm", | ||
| uuid=b"12345678123456781234567812345678", | ||
| ) | ||
| assert np.all(fp["tmatrix"] == m) | ||
| assert fp["angular_vacuum_wavenumber"][...] == 7 | ||
| assert np.all( | ||
| fp["materials/embedding/relative_permittivity"] | ||
| == np.arange(1, 5)[:, None] * [1, 1, 1] | ||
| ) | ||
| assert np.all( | ||
| fp["materials/embedding/relative_permeability"] == np.arange(1, 4) | ||
| ) | ||
| assert fp["materials/embedding/chirality"][...] == 0.5 | ||
| assert fp["embedding"] == fp["materials/embedding"] | ||
| assert np.all(fp["modes/l"] == tms[0][0].basis.l) | ||
| assert np.all(fp["modes/m"] == tms[0][0].basis.m) | ||
| assert np.all( | ||
| [i.decode() for i in fp["modes/polarization"][...]] | ||
| == 8 * ["positive", "negative"] | ||
| ) | ||
| assert fp["uuid"][()] == b"12345678123456781234567812345678" | ||
| assert fp.attrs["name"] == "testname" | ||
| assert fp.attrs["description"] == "testdescr" | ||
| assert fp["angular_vacuum_wavenumber"].attrs["unit"] == r"µm^{-1}" | ||
| def test_parity(self): | ||
| with h5py.File("test.h5", "x", driver="core", backing_store=False) as fp: | ||
| m = np.arange(4 * 3 * 16 * 16).reshape((4, 3, 16, 16)) | ||
| tms = [ | ||
| [ | ||
| treams.TMatrix( | ||
| m[i, ii], k0=i + 1, material=treams.Material(), poltype="parity" | ||
| ) | ||
| for ii in range(3) | ||
| ] | ||
| for i in range(4) | ||
| ] | ||
| io.save_hdf5(fp, tms, "testname", "testdescr") | ||
| assert ( | ||
| np.all(fp["tmatrix"] == m) | ||
| and np.all(fp["angular_vacuum_wavenumber"] == np.arange(1, 5)[:, None]) | ||
| and fp["materials/embedding/relative_permittivity"][...] == 1 | ||
| and fp["materials/embedding/relative_permeability"][...] == 1 | ||
| and fp["embedding"] == fp["materials/embedding"] | ||
| and np.all(fp["modes/l"] == tms[0][0].basis.l) | ||
| and np.all(fp["modes/m"] == tms[0][0].basis.m) | ||
| and np.all( | ||
| [i.decode() for i in fp["modes/polarization"][...]] | ||
| == 8 * ["electric", "magnetic"] | ||
| ) | ||
| ) | ||
| class TestLoadHdf5: | ||
| def test(self): | ||
| with h5py.File("test.h5", "x", driver="core", backing_store=False) as fp: | ||
| fp.create_dataset( | ||
| "tmatrix", data=np.arange(4 * 3 * 6 * 4).reshape((4, 3, 6, 4)) | ||
| ) | ||
| fp.create_dataset("frequency", data=np.arange(1, 5)[:, None]) | ||
| fp["frequency"].attrs["unit"] = "THz" | ||
| fp.create_dataset("materials/foo/refractive_index", data=4) | ||
| fp["embedding"] = h5py.SoftLink("/materials/foo") | ||
| fp.create_dataset("modes/positions", data=[[0, 0, 0]]) | ||
| fp.create_dataset("modes/l_incident", data=[1, 1, 2, 2]) | ||
| fp.create_dataset("modes/l_scattered", data=6 * [1]) | ||
| fp.create_dataset("modes/m_incident", data=4 * [0]) | ||
| fp.create_dataset("modes/m_scattered", data=[-1, -1, 0, 0, 1, 1]) | ||
| fp.create_dataset( | ||
| "modes/polarization_incident", data=2 * ["electric", "magnetic"] | ||
| ) | ||
| fp.create_dataset( | ||
| "modes/polarization_scattered", data=3 * ["electric", "magnetic"] | ||
| ) | ||
| tms = io.load_hdf5(fp) | ||
| basis = treams.SphericalWaveBasis( | ||
| [ | ||
| (1, 0, 0), | ||
| (1, 0, 1), | ||
| (2, 0, 0), | ||
| (2, 0, 1), | ||
| (1, -1, 0), | ||
| (1, -1, 1), | ||
| (1, 1, 0), | ||
| (1, 1, 1), | ||
| ] | ||
| ) | ||
| assert tms.shape == (4, 3) | ||
| assert abs(tms[0, 0].k0 - 2 * np.pi / 299792.458) < 1e-16 | ||
| assert abs(tms[0, 1].k0 - 2 * np.pi / 299792.458) < 1e-16 | ||
| assert abs(tms[1, 0].k0 - 4 * np.pi / 299792.458) < 1e-16 | ||
| assert tms[0, 0].poltype == "parity" | ||
| assert tms[0, 0].basis <= basis and basis <= tms[0, 0].basis | ||
| assert np.all( | ||
| tms[3, 2] | ||
| == [ | ||
| [272, 273, 274, 275, 0, 0, 0, 0], | ||
| [276, 277, 278, 279, 0, 0, 0, 0], | ||
| [0, 0, 0, 0, 0, 0, 0, 0], | ||
| [0, 0, 0, 0, 0, 0, 0, 0], | ||
| [264, 265, 266, 267, 0, 0, 0, 0], | ||
| [268, 269, 270, 271, 0, 0, 0, 0], | ||
| [280, 281, 282, 283, 0, 0, 0, 0], | ||
| [284, 285, 286, 287, 0, 0, 0, 0], | ||
| ] | ||
| ) |
| import numpy as np | ||
| import treams.lattice as la | ||
| def isclose(a, b, rel_tol=1e-06, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| EPS = 2e-7 | ||
| EPSSQ = 4e-14 | ||
| class TestLSumSW1d: | ||
| def test(self): | ||
| assert isclose( | ||
| la.lsumsw1d(7, 2, -0.2, 1.1, 0.5, 0), | ||
| 7933.055199668467 + 118032.50658666379j, | ||
| ) | ||
| def test_origin(self): | ||
| assert isclose( | ||
| la.lsumsw1d(0, 1 + 0.1j, -0.2, 1.1, 0, 0), | ||
| 0.46956307915218665 - 0.06082398908132698j, | ||
| ) | ||
| def test_zero(self): | ||
| assert la.lsumsw1d(1, 2, 0, 1, 0.5, 0) == 0 | ||
| def test_beta_zero_even(self): | ||
| assert isclose( | ||
| la.lsumsw1d(4, 2, 0, 1.1, 0.5, 0), 0.453184295682506 - 136.37405323742743j | ||
| ) | ||
| def test_beta_zero_odd(self): | ||
| assert isclose( | ||
| la.lsumsw1d(3, 2, 0, 1.1, 0.5, 0), | ||
| 9.949637075241129e-08 + 6.188903717006807j, | ||
| ) | ||
| class TestDSumSW1d: | ||
| def test(self): | ||
| assert isclose( | ||
| la.dsumsw1d(7, 2, -0.2, 1.1, 0.5, 1), 7926.666849705289 - 35407.6409574394j | ||
| ) | ||
| def test_zero(self): | ||
| assert la.dsumsw1d(7, 2j, -0.2, 1.1, 0, 0) == 0 | ||
| def test_i0(self): | ||
| assert isclose(la.dsumsw1d(7, 2j, -0.2, 1.1, 0.5, 0), 142089.72088031314j) | ||
| def test_edge(self): | ||
| assert isclose( | ||
| la.dsumsw1d(7, 2, -0.2, 1, 0.5, 0), 30486.115441831644 + 3058.814396120404j | ||
| ) | ||
| class TestLSumCW1d: | ||
| def test(self): | ||
| assert isclose( | ||
| la.lsumcw1d(-7, 2, -0.2, 1.1, 0.5, 0), | ||
| -1905.5519170004113 - 22107.5787204855j, | ||
| ) | ||
| def test_origin(self): | ||
| assert isclose( | ||
| la.lsumcw1d(0, 1 + 0.1j, -0.2, 1.1, 0, 0), | ||
| 0.9012865303927504 + 0.9760354794296119j, | ||
| ) | ||
| def test_zero(self): | ||
| assert la.lsumcw1d(1, 2, 0, 1, 0.5, 0) == 0 | ||
| def test_beta_zero_even(self): | ||
| assert isclose( | ||
| la.lsumcw1d(4, 2, 0, 1.1, 0.5, 0), 0.9090909089382382 - 51.211404621440536j | ||
| ) | ||
| def test_beta_zero_odd(self): | ||
| assert isclose( | ||
| la.lsumcw1d(3, 2, 0, 1.1, 0.5, 0), | ||
| -2.377282713998203e-10 + 2.306034974211807j, | ||
| ) | ||
| class TestDSumCW1d: | ||
| def test(self): | ||
| assert isclose( | ||
| la.dsumcw1d(-7, 2, -0.2, 1.1, 0.5, 1), | ||
| -1900.7331947651744 + 8473.777703044347j, | ||
| ) | ||
| def test_zero(self): | ||
| assert la.dsumcw1d(-7, 2j, -0.2, 1.1, 0, 0) == 0 | ||
| def test_i0(self): | ||
| assert isclose(la.dsumcw1d(-7, 2j, -0.2, 1.1, 0.5, 0), 28143.06322075269) | ||
| def test_edge(self): | ||
| assert isclose( | ||
| la.dsumcw1d(-7, 2, -0.2, 1, 0.5, 0), -6077.087627234397 - 609.742594614585j | ||
| ) | ||
| class TestLSumSW2d: | ||
| def test(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d(7, 3, 2, [0.1, 0.2], a, [0.2, 0], 0), | ||
| 51.04558205 - 68859783.71895242j, | ||
| ) | ||
| def test_origin(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d(0, 0, 1 + 0.1j, [0.1, 0.2], a, [0, 0], 0), | ||
| 1.2763907 + 0.65148623j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert la.lsumsw2d(7, 4, 2, [0.1, 0.2], a, [0, 0], 0) == 0 | ||
| def test_zero_2(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert la.lsumsw2d(7, 3, 2, [0, 0], a, [0, 0], 0) == 0 | ||
| def test_beta_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d(6, 0, 2, [0, 0], a, [0.2, 0], 0), -1.33422509 + 2031448.8128476j | ||
| ) | ||
| def test_beta_zero_odd(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d(7, 3, 2, [0, 0], a, [0.2, 0], 0), | ||
| 0.00296384 - 68859750.95204805j, | ||
| ) | ||
| class TestDSumSW2d: | ||
| def test(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.dsumsw2d(7, 3, 2, [0.1, 0.2], a, [0.2, 0], 1), | ||
| 50.744595676527425 + 469.33788946355355j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert la.dsumsw2d(7, 3, 2j, [0.1, 0.2], a, [0, 0], 0) == 0 | ||
| class TestLSumCW2d: | ||
| def test(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumcw2d(-7, 2, [0.1, 0.2], a, [0.2, 0], 0), | ||
| 72.4265408 - 18023963.86399937j, | ||
| ) | ||
| def test_origin(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumcw2d(7, 1 + 0.1j, [0.1, 0.2], a, [0, 0], 0), | ||
| 1856.54740513 + 8265.31298836j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert la.lsumcw2d(7, 2, [0, 0], a, [0, 0], 0) == 0 | ||
| def test_beta_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumcw2d(0, 2, [0, 0], a, [0.2, 0], 0), | ||
| 3.4057753716089e-7 + 0.4974331386852614j, | ||
| ) | ||
| def test_beta_zero_odd(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumcw2d(7, 2, [0, 0], a, [0.2, 0], 0), | ||
| 0.00013173070560507455 + 18023975.612865075j, | ||
| ) | ||
| class TestDSumCW2d: | ||
| def test(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.dsumcw2d(-7, 2, [0.1, 0.2], a, [0.2, 0], 1), | ||
| 71.68432125868046 + 811.2490404245293j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert la.dsumcw2d(-7, 2j, [0.1, 0.2], a, [0, 0], 0) == 0 | ||
| class TestLSumSW3d: | ||
| def test(self): | ||
| a = 1.1 * np.eye(3) | ||
| assert isclose( | ||
| la.lsumsw3d(7, 3, 2, [0.1, 0.2, -0.3], a, [0.2, -0.3, 0.4], 0), | ||
| -5410.987286636962 + 26890.112935162313j, | ||
| ) | ||
| def test_origin(self): | ||
| a = 1.1 * np.eye(3) | ||
| assert isclose( | ||
| la.lsumsw3d(0, 0, 1 + 0.1j, [0.1, 0.2, -0.3], a, [0, 0, 0], 0), | ||
| 0.7797387103128192 + 3.546723626318176j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(3) | ||
| assert la.lsumsw3d(1, 0, 2, [0, 0, 0], a, [0, 0, 0], 0) == 0 | ||
| def test_beta_zero(self): | ||
| a = 1.1 * np.eye(3) | ||
| assert isclose( | ||
| la.lsumsw3d(6, 3, 2, [0, 0, 0], a, [0.2, -0.3, 0.4], 0), | ||
| 475.3946724930312 - 2658.101604143323j, | ||
| ) | ||
| class TestDSumSW3d: | ||
| def test(self): | ||
| a = 1.1 * np.eye(3) | ||
| assert isclose( | ||
| la.dsumsw3d(7, 3, 2, [0.1, 0.2, -0.3], a, [0.2, -0.3, 0.4], 1), | ||
| -376.09928123585945 + 1161.1103743114297j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(3) | ||
| assert la.dsumsw3d(7, 3, 2j, [0.1, 0.2, -0.3], a, [0, 0, 0], 0) == 0 | ||
| class TestLSumSW2dShift: | ||
| def test(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d_shift(7, 4, 2, [0.1, 0.2], a, [0.2, 0.2, 0.1], 0), | ||
| -46.2822843 + 2452401.72386332j, | ||
| ) | ||
| def test2(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d_shift(6, 2, 1 + 0.1j, [0.1, 0.2], a, [0.2, 0.2, 0.1], 0), | ||
| -6065646.19932015 + 5077087.337158j, | ||
| ) | ||
| def test_singular(self): | ||
| a = np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d_shift(6, 0, 2 * np.pi, [0, 0], a, [0.2, 0.2, 0.1], 0), | ||
| 554145.857367349-554173.9024304616j, | ||
| ) | ||
| def test_z_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d_shift(0, 0, 2, [0.1, 0.2], a, [0.2, 0, 0], 0), | ||
| 0.37395298646674273 - 0.2501854057015295j, | ||
| ) | ||
| def test_beta_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.lsumsw2d_shift(6, 0, 2, [0, 0], a, [0.2, 0.2, 0.1], 0), | ||
| -1.30733661 - 74158.23878748j, | ||
| ) | ||
| class TestDSumSW2dShift: | ||
| def test(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert isclose( | ||
| la.dsumsw2d_shift(7, 4, 2, [0.1, 0.2], a, [0.2, 0.2, 0.1], 1), | ||
| -46.14178891555638 - 327.2420554824938j, | ||
| ) | ||
| def test_zero(self): | ||
| a = 1.1 * np.eye(2) | ||
| assert la.dsumsw2d_shift(7, 3, 2j, [0.1, 0.2], a, [0, 0, 0], 0) == 0 | ||
| class TestLSumSW1dShift: | ||
| def test(self): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(7, 4, 2, 0.1, 1, [0.2, 0.2, 0.1], 0), | ||
| 5.410572060609522 + 2452771.8911033254j, | ||
| ) | ||
| def test2(self): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(6, 2, 1 + 0.1j, 0.1, 1.1, [0.2, 0.2, 0.1], 0), | ||
| -6061553.746083904 + 5072912.632902036j, | ||
| ) | ||
| def test_singular(self): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(6, 0, 2 * np.pi, 0, 1, [0.2, 0.2, 0.1], 0), | ||
| -0.3552862673579646-24.943884092634878j, | ||
| ) | ||
| def test_xy_zero(self): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(0, 0, 2, 0.1, 1.1, [0, 0, 0.2], 0), | ||
| 0.4025419802107214 - 0.5283600560367392j, | ||
| ) | ||
| def test_xy_zero_2(self): | ||
| assert la.lsumsw1d_shift(3, 1, 2, 0.1, 1.1, [0, 0, 0.2], 0) == 0 | ||
| def test_beta_zero(self): | ||
| assert isclose( | ||
| la.lsumsw1d_shift(6, 0, 2, 0, 1.1, [0.2, 0.2, 0.1], 0), | ||
| 0.41829203130234643 - 74299.44379903575j, | ||
| ) | ||
| class TestDSumSW1dShift: | ||
| def test(self): | ||
| assert isclose( | ||
| la.dsumsw1d_shift(7, 4, 2, 0.1, 1, [0.2, 0.2, 0.1], 1), | ||
| 5.405752406639203 + 42.20701547695025j, | ||
| ) | ||
| def test_zero(self): | ||
| assert la.dsumsw1d_shift(7, 4, 2j, 0.1, 1, [0, 0, 0], 0) == 0 | ||
| def test_dispatch(self): | ||
| assert isclose( | ||
| la.dsumsw1d_shift(7, 0, 2j, -0.2, 1.1, [0.5, 0, 0], 0), 142089.72088031314j | ||
| ) | ||
| def test_i0(self): | ||
| assert isclose( | ||
| la.dsumsw1d_shift(7, 3, 2, -0.2, 1, [0.1, 0.2, 0.5], 0), | ||
| -4348.28631675554 + 23915.57474036112j, | ||
| ) | ||
| class TestLSumCW1dShift: | ||
| def test(self): | ||
| assert isclose( | ||
| la.lsumcw1d_shift(-7, 2, 0.1, 1, [0.2, 0.1], 0.5), | ||
| 857009.7602939741 + 8224316.447124034j, | ||
| ) | ||
| def test2(self): | ||
| assert isclose( | ||
| la.lsumcw1d_shift(6, 1 + 0.1j, 0.1, 1.1, [0.2, 0.1], 0), | ||
| 15553417.521558769 + 10955244.470110876j, | ||
| ) | ||
| def test_singular(self): | ||
| assert isclose( | ||
| la.lsumcw1d_shift(6, 2 * np.pi, 0, 1, [0.2, 0.1], 0), | ||
| -1743317.7346371387+1743768.8654999665j, | ||
| 1e-5, | ||
| ) | ||
| def test_xy_zero(self): | ||
| assert isclose( | ||
| la.lsumcw1d_shift(0, 2, 0.1, 1.1, [0.2, 0], 0), | ||
| 0.9243271013171527 + 0.017729454351493944j, | ||
| ) | ||
| def test_beta_zero(self): | ||
| assert isclose( | ||
| la.lsumcw1d_shift(6, 2, 0, 1.1, [0.2, 0.1], 0), | ||
| 108600.73717056998 + 288821.87794970983j, | ||
| ) | ||
| class TestDSumCW1dShift: | ||
| def test(self): | ||
| assert isclose( | ||
| la.dsumcw1d_shift(-7, 2, 0.1, 1, [0.2, 0.1], 1), | ||
| -841.490999741377 + 757.8694926475844j, | ||
| ) | ||
| def test_zero(self): | ||
| assert la.dsumcw1d_shift(-7, 2j, 0.1, 1, [0, 0], 0) == 0 | ||
| def test_i0(self): | ||
| assert isclose(la.dsumcw1d_shift(-7, 2, -0.2, 1, [0, 0.5], 0), 30588.957052124) | ||
| class TestArea: | ||
| def test(self): | ||
| assert la.area([[1, 2], [3, 4.0]]) == -2 | ||
| class TestVolume: | ||
| def test_2d(self): | ||
| assert la.volume([[1, 2], [3, 4]]) == -2 | ||
| def test_3d(self): | ||
| assert la.volume([[1, 2, 3], [4, 5, 6], [7, 8, 10]]) == -3 | ||
| def test_3d_2(self): | ||
| assert la.volume(np.eye(3)) == 1 | ||
| class TestReciprocal: | ||
| def test_2d(self): | ||
| res = la.reciprocal(3 * np.eye(2)).flatten() | ||
| expect = (np.eye(2) * 2 * np.pi / 3).flatten() | ||
| assert np.all(np.abs(res - expect) < EPSSQ) | ||
| def test_3d(self): | ||
| a = [[1, 2, 3], [4, 5, 6], [7, 8, 10]] | ||
| res = la.reciprocal(a) | ||
| expect = 2 * np.pi / (-3) * np.array([[2, 2, -3], [4, -11, 6], [-3, 6, -3]]) | ||
| assert np.all(np.abs(res - expect) < EPSSQ) | ||
| class TestDiffrOrdersCircle: | ||
| def test_0(self): | ||
| assert np.array_equal(la.diffr_orders_circle(np.eye(2), 0), [[0, 0]]) | ||
| def test_square(self): | ||
| assert np.array_equal( | ||
| la.diffr_orders_circle(np.eye(2), 1.5), | ||
| [ | ||
| [0, 0], | ||
| [0, 1], | ||
| [0, -1], | ||
| [1, 0], | ||
| [-1, 0], | ||
| [1, 1], | ||
| [-1, -1], | ||
| [1, -1], | ||
| [-1, 1], | ||
| ], | ||
| ) | ||
| def test_hex(self): | ||
| assert np.array_equal( | ||
| la.diffr_orders_circle([[1, 0], [0.5, np.sqrt(0.75)]], 1.1), | ||
| [[0, 0], [0, 1], [0, -1], [1, 0], [-1, 0], [1, -1], [-1, 1]], | ||
| ) | ||
| def test_neg(self): | ||
| assert la.diffr_orders_circle(np.eye(2), -1).size == 0 | ||
| class TestCube: | ||
| def test(self): | ||
| assert np.all( | ||
| la.cube(2, 1) | ||
| == [ | ||
| [-1, -1], | ||
| [-1, 0], | ||
| [-1, 1], | ||
| [0, -1], | ||
| [0, 0], | ||
| [0, 1], | ||
| [1, -1], | ||
| [1, 0], | ||
| [1, 1], | ||
| ] | ||
| ) | ||
| class TestCubeEdge: | ||
| def test(self): | ||
| assert np.all( | ||
| la.cubeedge(2, 1) | ||
| == [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] | ||
| ) |
| import numpy as np | ||
| import pytest | ||
| from treams import Lattice, WaveVector | ||
| class TestLattice: | ||
| def test_init(self): | ||
| assert Lattice([1]) == Lattice(1, alignment="z") | ||
| def test_init_lattice(self): | ||
| assert Lattice(Lattice([1, 2])) == Lattice([[1, 0], [0, 2]]) | ||
| def test_init_invalid_shape(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice([0, 1, 2, 3]) | ||
| def test_init_invalid_alignment(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(1, "a") | ||
| def test_init_volume(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice([[1, 2], [2, 4]]) | ||
| def test_square(self): | ||
| assert Lattice.square(1) == Lattice([1, 1]) | ||
| def test_cubic(self): | ||
| assert Lattice.cubic(1) == Lattice([1, 1, 1]) | ||
| def test_rectangular(self): | ||
| assert Lattice.rectangular(1, 2) == Lattice([1, 2]) | ||
| def test_orthorhombic(self): | ||
| assert Lattice.orthorhombic(1, 2, 3) == Lattice([1, 2, 3]) | ||
| def test_hexagonal_2d(self): | ||
| assert Lattice.hexagonal(1) == Lattice([[1, 0], [0.5, np.sqrt(0.75)]]) | ||
| def test_hexagonal_3d(self): | ||
| assert Lattice.hexagonal(1, 2) == Lattice( | ||
| [[1, 0, 0], [0.5, 0.75 ** 0.5, 0], [0, 0, 2]] | ||
| ) | ||
| def test_reciprocal(self): | ||
| assert (Lattice([np.pi, 2 * np.pi]).reciprocal == [[2, 0], [0, 1]]).all() | ||
| def test_str(self): | ||
| assert str(Lattice(1)) == "1.0" | ||
| def test_repr(self): | ||
| assert ( | ||
| repr(Lattice([1, 2])) | ||
| == """Lattice([[1. 0.] | ||
| [0. 2.]], alignment='xy')""" | ||
| ) | ||
| def test_sublattice(self): | ||
| assert Lattice(Lattice([1, 2, 3]), "xy") == Lattice([1, 2]) | ||
| def test_sublattice_1d(self): | ||
| assert Lattice(Lattice(1), "z") == Lattice(1) | ||
| def test_sublattice_1d_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(Lattice(1), "x") | ||
| def test_sublattice_2d_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(Lattice([1, 2], "zx"), "xy") | ||
| def test_sublattice_find_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(Lattice([[1, 1], [0, 1]]), "x") | ||
| def test_permute(self): | ||
| assert Lattice([1, 2]).permute(4) == Lattice([1, 2], "yz") | ||
| def test_permute_3d(self): | ||
| assert Lattice([1, 2, 3]).permute(2.0) == Lattice( | ||
| [[0, 0, 1], [2, 0, 0], [0, 3, 0]] | ||
| ) | ||
| def test_permute_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(1).permute(0.2) | ||
| def test_bool(self): | ||
| assert bool(Lattice(1)) | ||
| def test_or(self): | ||
| assert Lattice(1) | Lattice([2, 3]) == Lattice([2, 3, 1]) | ||
| def test_or_none(self): | ||
| assert Lattice(1) | None == Lattice(1) | ||
| def test_or_1d(self): | ||
| assert Lattice(1) | Lattice(2, "x") == Lattice([1, 2], "zx") | ||
| def test_or_1d_swapped(self): | ||
| assert Lattice(1, "x") | Lattice(2) == Lattice([2, 1], "zx") | ||
| def test_or_2d(self): | ||
| assert Lattice([1, 2]) | Lattice(1, "x") == Lattice([1, 2]) | ||
| def test_or_2d_swapped(self): | ||
| assert Lattice(1, "x") | Lattice([1, 2]) == Lattice([1, 2]) | ||
| def test_or_3d(self): | ||
| assert Lattice([1, 2, 3]) | Lattice(1, "x") == Lattice([1, 2, 3]) | ||
| def test_or_3d_swapped(self): | ||
| assert Lattice(2, "y") | Lattice([1, 2, 3]) == Lattice([1, 2, 3]) | ||
| def test_or_3d_2d(self): | ||
| assert Lattice([1, 2]) | Lattice([2, 3], "yz") == Lattice([1, 2, 3]) | ||
| def test_or_3d_2d_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice([1, 2]) | Lattice([3, 3], "yz") | ||
| def test_or_3d_x(self): | ||
| assert Lattice(1, "x") | Lattice([2, 3], "yz") == Lattice([1, 2, 3]) | ||
| def test_or_3d_y(self): | ||
| assert Lattice(2, "y") | Lattice([3, 1], "zx") == Lattice([1, 2, 3]) | ||
| def test_or_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(1) | Lattice(2) | ||
| def test_and_none(self): | ||
| assert Lattice(1) & None is None | ||
| def test_and_same(self): | ||
| assert Lattice(1) & Lattice(1) == Lattice(1) | ||
| def test_and_empty(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(1) & Lattice(2) | ||
| def test_and_error(self): | ||
| with pytest.raises(ValueError): | ||
| Lattice(1) & Lattice(1, "x") | ||
| def test_and(self): | ||
| assert Lattice([1, 2, 3]) & Lattice([[0, 1], [3, 0]], "zx") == Lattice( | ||
| [[0, 1], [3, 0]], "zx" | ||
| ) | ||
| def test_le(self): | ||
| assert Lattice(3) <= Lattice([1, 2, 3]) | ||
| def test_le_false(self): | ||
| assert not Lattice(3) >= Lattice([1, 2, 3]) | ||
| def test_isdisjoint(self): | ||
| assert Lattice(1).isdisjoint(Lattice([1, 1])) | ||
| def test_isdisjoint_false(self): | ||
| assert not Lattice(1).isdisjoint(Lattice([1, 1, 1])) |
| import numpy as np | ||
| from treams import misc | ||
| class TestRefractiveIndex: | ||
| def test_single(self): | ||
| assert np.all(misc.refractive_index() == [1, 1]) | ||
| def test_multiple(self): | ||
| expect = [ | ||
| [[0.5, 1.5], [-np.sqrt(2) + 1j, np.sqrt(2) + 1j]], | ||
| [[np.sqrt(3) - 0.5, np.sqrt(3) + 0.5], [1j, 3j]], | ||
| ] | ||
| assert np.all( | ||
| misc.refractive_index(mu=[[1, 2], [3, -4 + 0j]], kappa=[0.5, 1j]) == expect | ||
| ) | ||
| class TestBasisChange: | ||
| def test(self): | ||
| assert np.all( | ||
| misc.basischange(([1, 1], [0, 1])) | ||
| == np.sqrt(0.5) * np.array([[-1, 1], [1, 1]]) | ||
| ) | ||
| class TestPickModes: | ||
| def test(self): | ||
| assert np.all(misc.pickmodes(([1, 1], [0, 1]), ([1], [0])) == [[1], [0]]) | ||
| class TestWaveVecZ: | ||
| def test(self): | ||
| assert misc.wave_vec_z(3, 4, 5) == 0 | ||
| def test_neg_single(self): | ||
| assert misc.wave_vec_z(0, 0, 2 - 2j) == -2 + 2j | ||
| def test_multiple(self): | ||
| assert np.all(misc.wave_vec_z([3, 3], [0, 5], 5) == [4, 3j]) | ||
| class TestFirstBrillouin1d: | ||
| def test(self): | ||
| assert np.abs(misc.firstbrillouin1d(0.81, 0.5) + 0.19) < 1e-16 | ||
| def test_edge(self): | ||
| assert misc.firstbrillouin1d(-0.25, 0.5) == 0.25 | ||
| class TestFirstBrillouin2d: | ||
| def test(self): | ||
| assert np.all( | ||
| np.abs( | ||
| misc.firstbrillouin2d([1.81, -0.25], 0.5 * np.eye(2)) - [-0.19, -0.25] | ||
| ) | ||
| < 1e-16 | ||
| ) | ||
| class TestFirstBrillouin3d: | ||
| def test(self): | ||
| assert np.all( | ||
| np.abs( | ||
| misc.firstbrillouin3d([1.81, -0.25, 0], 0.5 * np.eye(3)) | ||
| - [-0.19, -0.25, 0] | ||
| ) | ||
| < 1e-16 | ||
| ) |
| import numpy as np | ||
| import pytest | ||
| import treams | ||
| import treams.special as sc | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestRotate: | ||
| def test_sw_invalid(self): | ||
| a = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| with pytest.raises(AttributeError): | ||
| treams.rotate(1, basis=(b, a)) | ||
| def test_sw(self): | ||
| a = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.rotate(1, 2, 3, basis=(a, b), where=where) | ||
| y = treams.PhysicsArray( | ||
| [[treams.sw.rotate(1, 0, 0, 1, -1, 0, 1, 2, 3), 0]], basis=(a, b) | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw(self): | ||
| a = treams.CylindricalWaveBasis([[0.1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis([[0.1, 0, 0], [0.1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.rotate(2, basis=(a, b), where=where) | ||
| y = treams.PhysicsArray( | ||
| [[treams.cw.rotate(0.1, 0, 0, 0.1, 0, 0, 2), 0]], basis=(a, b) | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw_invalid(self): | ||
| a = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| with pytest.raises(AttributeError): | ||
| treams.rotate(1, basis=(a, b)) | ||
| def test_pw(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [0, 1, 0, 0]]) | ||
| a = treams.PlaneWaveBasisByUnitVector( | ||
| [[np.cos(2), np.sin(2), 0, 0], [-np.sin(2), np.cos(2), 0, 0]] | ||
| ) | ||
| where = [True, False] | ||
| x = treams.rotate(2, basis=b, where=where) | ||
| y = treams.PhysicsArray([[1, 0], [0, 0]], basis=(a, b)) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pwp(self): | ||
| b = treams.PlaneWaveBasisByComp([[1, 0, 0], [0, 1, 0]]) | ||
| a = treams.PlaneWaveBasisByComp( | ||
| [[np.cos(2), np.sin(2), 0], [-np.sin(2), np.cos(2), 0]] | ||
| ) | ||
| where = [True, False] | ||
| x = treams.rotate(2, basis=b, where=where) | ||
| y = treams.PhysicsArray([[1, 0], [0, 0]], basis=(a, b)) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_matmul(self): | ||
| assert ( | ||
| treams.Rotate(1, 2, 3) | ||
| @ treams.PhysicsArray(np.eye(6), basis=treams.SphericalWaveBasis.default(1)) | ||
| == treams.rotate(1, 2, 3, basis=treams.SphericalWaveBasis.default(1)) | ||
| ).all() | ||
| def test_rmatmul(self): | ||
| assert ( | ||
| treams.PhysicsArray(np.eye(6), basis=treams.SphericalWaveBasis.default(1)) | ||
| @ treams.Rotate(1, 2, 3) | ||
| == treams.rotate(1, 2, 3, basis=treams.SphericalWaveBasis.default(1)) | ||
| ).all() | ||
| class TestTranslate: | ||
| def test_sw(self): | ||
| a = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.translate( | ||
| [[0, 0, 0], [0, 1, 1]], k0=3, basis=(a, b), material=(1, 2, 0), where=where | ||
| ) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [[treams.sw.translate(1, 0, 0, 1, -1, 0, 0, 0, 0, singular=False), 0]], | ||
| [ | ||
| [ | ||
| treams.sw.translate( | ||
| 1, 0, 0, 1, -1, 0, 6, np.pi / 4, np.pi / 2, singular=False | ||
| ), | ||
| 0, | ||
| ] | ||
| ], | ||
| ], | ||
| basis=(None, a, b), | ||
| poltype=(None, "helicity", "helicity"), | ||
| k0=(None, 3, 3), | ||
| material=(None,) + 2 * (treams.Material(1, 2, 0),), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_sw_invalid(self): | ||
| a = treams.SphericalWaveBasis([[1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| with pytest.raises(AttributeError): | ||
| treams.translate([1, 0, 0], k0=1, basis=(b, a)) | ||
| def test_invalid_r(self): | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| with pytest.raises(ValueError): | ||
| treams.translate([1, 0], basis=b, k0=1) | ||
| def test_cw(self): | ||
| a = treams.CylindricalWaveBasis([[0.1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis([[0.1, -1, 0], [0.1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.translate( | ||
| [[0, 0, 0], [0, 1, 1]], k0=3, basis=(a, b), material=(1, 2, 0), where=where | ||
| ) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| [ | ||
| treams.cw.translate( | ||
| 0.1, 0, 0, 0.1, -1, 0, 0, 0, 0, singular=False | ||
| ), | ||
| 0, | ||
| ] | ||
| ], | ||
| [ | ||
| [ | ||
| treams.cw.translate( | ||
| 0.1, | ||
| 0, | ||
| 0, | ||
| 0.1, | ||
| -1, | ||
| 0, | ||
| np.sqrt(18 - 0.01), | ||
| np.pi / 2, | ||
| 1, | ||
| singular=False, | ||
| ), | ||
| 0, | ||
| ] | ||
| ], | ||
| ], | ||
| basis=(None, a, b), | ||
| k0=(None, 3, 3), | ||
| material=(None,) + 2 * (treams.Material(1, 2, 0),), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pw(self): | ||
| a = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0]]) | ||
| b = treams.PlaneWaveBasisByUnitVector([[1, 0, 0, 0], [0.6, 0.8, 0, 0]]) | ||
| where = [True, False] | ||
| x = treams.translate([[0, 0, 0], [1, 1, 1]], k0=1, basis=(a, b), where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [[treams.pw.translate(1, 0, 0, 0, 0, 0), 0]], | ||
| [[treams.pw.translate(1, 0, 0, 1, 1, 1), 0]], | ||
| ], | ||
| basis=(None, a, b), | ||
| k0=(1, 1), | ||
| material=((), ()), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pwp(self): | ||
| a = treams.PlaneWaveBasisByComp([[4, 0, 0]]) | ||
| b = treams.PlaneWaveBasisByComp([[4, 0, 0], [4, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.translate([[0, 0, 0], [0, 1, 1]], k0=5, basis=(a, b), where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [[treams.pw.translate(4, 0, 3, 0, 0, 0), 0]], | ||
| [[treams.pw.translate(4, 0, 3, 0, 1, 1), 0]], | ||
| ], | ||
| basis=(None, a, b), | ||
| k0=(None, 5, 5), | ||
| material=(None, treams.Material(), treams.Material()), | ||
| modetype=(None, "up", "up"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| class TestExpand: | ||
| def test_sw_sw_sing(self): | ||
| a = treams.SphericalWaveBasis([[2, 0, 0]], [0, 1, 1]) | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.expand( | ||
| (a, b), ("regular", "singular"), k0=3, material=(1, 2, 0), where=where | ||
| ) | ||
| y = treams.PhysicsArray( | ||
| [[treams.sw.translate(2, 0, 0, 1, -1, 0, 6, 0.25 * np.pi, 0.5 * np.pi), 0]], | ||
| basis=(a, b), | ||
| poltype="helicity", | ||
| k0=3, | ||
| material=treams.Material(1, 2, 0), | ||
| modetype=("regular", "singular"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw_cw_sing(self): | ||
| a = treams.CylindricalWaveBasis([[0.2, 0, 0]], [0, 1, 1]) | ||
| b = treams.CylindricalWaveBasis([[0.2, -1, 0], [0.2, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.expand( | ||
| (a, b), ("regular", "singular"), k0=3, material=(1, 2, 0), where=where | ||
| ) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.cw.translate( | ||
| 0.2, 0, 0, 0.2, -1, 0, np.sqrt(18 - 0.04), np.pi / 2, 1 | ||
| ), | ||
| 0, | ||
| ] | ||
| ], | ||
| basis=(a, b), | ||
| k0=3, | ||
| material=treams.Material(1, 2, 0), | ||
| modetype=("regular", "singular"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_sw_cw(self): | ||
| a = treams.SphericalWaveBasis([[1, 1, 0]]) | ||
| b = treams.CylindricalWaveBasis([[0.3, 1, 0], [0.1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.expand((a, b), k0=3, material=(1, 4, 0), where=where) | ||
| y = treams.PhysicsArray( | ||
| [[treams.cw.to_sw(1, 1, 0, 0.3, 1, 0, 6), 0]], | ||
| basis=(a, b), | ||
| poltype="helicity", | ||
| k0=3, | ||
| material=treams.Material(1, 4, 0), | ||
| modetype="regular", | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pw_pw(self): | ||
| a = treams.PlaneWaveBasisByUnitVector([[3, 0, 4, 0], [0, 0, 5, 0]]) | ||
| b = treams.PlaneWaveBasisByUnitVector([[3, 0, 4, 0], [0, 5, 0, 0]]) | ||
| where = [True, False] | ||
| x = treams.expand((a, b), k0=2.5, material=(2, 2, 0), where=where) | ||
| y = treams.PhysicsArray( | ||
| [[1, 0], [0, 0]], basis=(a, b), k0=2.5, material=treams.Material(2, 2, 0), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw_pw(self): | ||
| a = treams.CylindricalWaveBasis([[3, 1, 0]], [1, 2, 3]) | ||
| b = treams.PlaneWaveBasisByUnitVector([[0, 4, 3, 0], [0, 4, 3, 1]]) | ||
| where = [True, False] | ||
| x = treams.expand((a, b), k0=5, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.pw.to_cw(3, 1, 0, 0, 4, 3, 0) | ||
| * treams.pw.translate(0, 4, 3, 1, 2, 3), | ||
| 0, | ||
| ] | ||
| ], | ||
| basis=(a, b), | ||
| k0=5, | ||
| material=treams.Material(), | ||
| modetype=("regular", None), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_sw_pw(self): | ||
| a = treams.SphericalWaveBasis([[3, 1, 0]], [1, 2, 3]) | ||
| b = treams.PlaneWaveBasisByUnitVector([[0, 4, 3, 0], [0, 4, 3, 1]]) | ||
| where = [True, False] | ||
| x = treams.expand((a, b), k0=5, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.pw.to_sw(3, 1, 0, 0, 4, 3, 0) | ||
| * treams.pw.translate(0, 4, 3, 1, 2, 3), | ||
| 0, | ||
| ] | ||
| ], | ||
| basis=(a, b), | ||
| poltype="helicity", | ||
| k0=5, | ||
| material=treams.Material(), | ||
| modetype=("regular", None), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| class TestExpandLattice: | ||
| def test_sw_1d(self): | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [[True, False], [False, False]] | ||
| lattice = treams.Lattice(1) | ||
| x = treams.expandlattice(lattice, 0, basis=b, k0=3, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.sw.translate_periodic( | ||
| 3, 0, lattice[...], [0, 0, 0], [[1], [-1], [0]] | ||
| )[0, 0], | ||
| 0, | ||
| ], | ||
| [0, 0], | ||
| ], | ||
| basis=b, | ||
| poltype="helicity", | ||
| k0=3, | ||
| material=treams._material.Material(), | ||
| modetype=("regular", "singular"), | ||
| kpar=[np.nan, np.nan, 0], | ||
| lattice=lattice, | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_sw_2d(self): | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [[True, False], [False, False]] | ||
| lattice = treams.Lattice([[1, 0], [0, 1]]) | ||
| x = treams.expandlattice(lattice, [0, 0], basis=b, k0=3, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.sw.translate_periodic( | ||
| 3, [0, 0], lattice[...], [0, 0, 0], [[1], [-1], [0]] | ||
| )[0, 0], | ||
| 0, | ||
| ], | ||
| [0, 0], | ||
| ], | ||
| basis=b, | ||
| poltype="helicity", | ||
| k0=3, | ||
| material=treams._material.Material(), | ||
| modetype=("regular", "singular"), | ||
| kpar=[0, 0, np.nan], | ||
| lattice=lattice, | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_sw_3d(self): | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [[True, False], [False, False]] | ||
| lattice = treams.Lattice(np.eye(3)) | ||
| x = treams.expandlattice(lattice, [0, 0, 0], basis=b, k0=3, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.sw.translate_periodic( | ||
| 3, [0, 0, 0], lattice[...], [0, 0, 0], [[1], [-1], [0]] | ||
| )[0, 0], | ||
| 0, | ||
| ], | ||
| [0, 0], | ||
| ], | ||
| basis=b, | ||
| poltype="helicity", | ||
| k0=3, | ||
| material=treams._material.Material(), | ||
| modetype=("regular", "singular"), | ||
| kpar=[0, 0, 0], | ||
| lattice=lattice, | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw_sw(self): | ||
| a = treams.CylindricalWaveBasis([[0.3, 2, 0]]) | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [True, False] | ||
| x = treams.expandlattice(2, basis=(a, b), k0=3, material=(1, 4, 0), where=where) | ||
| y = treams.PhysicsArray( | ||
| [[treams.sw.periodic_to_cw(2, 0, 0, 1, -1, 0, 6, 2), 0]], | ||
| basis=(a, b), | ||
| poltype="helicity", | ||
| k0=3, | ||
| material=treams.Material(1, 4, 0), | ||
| modetype="singular", | ||
| lattice=treams.Lattice(2), | ||
| kpar=[np.nan, np.nan, 0.3], | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pw_sw(self): | ||
| a = treams.PlaneWaveBasisByUnitVector([[3, 0, 4, 0]]) | ||
| b = treams.SphericalWaveBasis([[1, -1, 0], [1, 1, 0]]) | ||
| where = [True, False] | ||
| lattice = treams.Lattice([[2, 0], [0, 2]]) | ||
| x = treams.expandlattice( | ||
| lattice, [3, 0], basis=(a, b), k0=2.5, material=(1, 4, 0), where=where | ||
| ) | ||
| y = treams.PhysicsArray( | ||
| [[treams.sw.periodic_to_pw(3, 0, 4, 0, 1, -1, 0, 4), 0]], | ||
| basis=(a, b), | ||
| k0=2.5, | ||
| kpar=[3, 0, np.nan], | ||
| lattice=lattice, | ||
| material=treams.Material(1, 4, 0), | ||
| modetype=(None, "singular"), | ||
| poltype="helicity", | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw_1d(self): | ||
| b = treams.CylindricalWaveBasis([[0.1, -1, 0], [0.1, 1, 0]]) | ||
| where = [[True, False], [False, False]] | ||
| lattice = treams.Lattice(1, "x") | ||
| x = treams.expandlattice(lattice, 0, basis=b, k0=3, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.cw.translate_periodic( | ||
| 3, 0, lattice[...], [0, 0, 0], [[0.1], [-1], [0]] | ||
| )[0, 0], | ||
| 0, | ||
| ], | ||
| [0, 0], | ||
| ], | ||
| basis=b, | ||
| k0=3, | ||
| material=treams._material.Material(), | ||
| modetype=("regular", "singular"), | ||
| kpar=[0, np.nan, 0.1], | ||
| lattice=lattice, | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw_2d(self): | ||
| b = treams.CylindricalWaveBasis([[0.1, -1, 0], [0.1, 1, 0]]) | ||
| where = [[True, False], [False, False]] | ||
| lattice = treams.Lattice([[1, 0], [0, 1]]) | ||
| x = treams.expandlattice(lattice, [0, 0], basis=b, k0=3, where=where) | ||
| y = treams.PhysicsArray( | ||
| [ | ||
| [ | ||
| treams.cw.translate_periodic( | ||
| 3, [0, 0], lattice[...], [0, 0, 0], [[0.1], [-1], [0]] | ||
| )[0, 0], | ||
| 0, | ||
| ], | ||
| [0, 0], | ||
| ], | ||
| basis=b, | ||
| k0=3, | ||
| material=treams._material.Material(), | ||
| modetype=("regular", "singular"), | ||
| kpar=[0, 0, 0.1], | ||
| lattice=lattice, | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pw_cw(self): | ||
| a = treams.PlaneWaveBasisByUnitVector([[3, 0, 4, 0]]) | ||
| b = treams.CylindricalWaveBasis([[4, -1, 0], [4, 1, 0]]) | ||
| where = [True, False] | ||
| lattice = treams.Lattice(2, "x") | ||
| x = treams.expandlattice( | ||
| lattice, 3, basis=(a, b), k0=2.5, material=(1, 4, 0), where=where | ||
| ) | ||
| y = treams.PhysicsArray( | ||
| [[treams.cw.periodic_to_pw(3, 0, 4, 0, 4, -1, 0, 2), 0]], | ||
| basis=(a, b), | ||
| k0=2.5, | ||
| material=treams.Material(1, 4, 0), | ||
| modetype=(None, "singular"), | ||
| lattice=lattice, | ||
| kpar=[3, np.nan, np.nan], | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| class TestChangePoltype: | ||
| def test_sw(self): | ||
| b = treams.CylindricalWaveBasis([[2, 0, 0], [1, 0, 1]]) | ||
| where = [True, False] | ||
| x = treams.changepoltype(basis=b, where=where) | ||
| y = treams.PhysicsArray( | ||
| [[-np.sqrt(0.5), 0], [0, 0]], basis=(b, b), poltype=("helicity", "parity"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_cw(self): | ||
| a = treams.SphericalWaveBasis([[2, 0, 0], [1, 0, 1]]) | ||
| b = treams.SphericalWaveBasis([[2, 0, 0], [1, 0, 1]]) | ||
| where = [True, False] | ||
| x = treams.changepoltype(basis=(a, b), where=where) | ||
| y = treams.PhysicsArray( | ||
| [[-np.sqrt(0.5), 0], [0, 0]], basis=(a, b), poltype=("helicity", "parity"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pw(self): | ||
| b = treams.PlaneWaveBasisByUnitVector([[2, 0, 0, 0], [1, 1, 0, 1]]) | ||
| where = [True, False] | ||
| x = treams.changepoltype(basis=b, where=where) | ||
| y = treams.PhysicsArray( | ||
| [[-np.sqrt(0.5), 0], [0, 0]], basis=(b, b), poltype=("helicity", "parity"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| def test_pwp(self): | ||
| b = treams.PlaneWaveBasisByComp([[2, 0, 0], [1, 0, 1]]) | ||
| where = [True, False] | ||
| x = treams.changepoltype(basis=b, where=where) | ||
| y = treams.PhysicsArray( | ||
| [[-np.sqrt(0.5), 0], [0, 0]], basis=(b, b), poltype=("helicity", "parity"), | ||
| ) | ||
| assert np.all(np.abs(x - y) < 1e-14) and x.ann == y.ann | ||
| class TestPermute: | ||
| def test_pw(self): | ||
| a = treams.PlaneWaveBasisByUnitVector([[1, 2, 3, 1], [1, 2, 3, 0]]) | ||
| b = treams.PlaneWaveBasisByUnitVector([[2, 3, 1, 1], [2, 3, 1, 0]]) | ||
| assert treams.permute(basis=b).basis[0] == a | ||
| def test_pwp(self): | ||
| a = treams.PlaneWaveBasisByComp([[1, 2, 1], [1, 2, 0]], "yz") | ||
| b = treams.PlaneWaveBasisByComp([[1, 2, 1], [1, 2, 0]]) | ||
| assert treams.permute(basis=b, k0=5, material=1).basis[0] == a | ||
| class TestEField: | ||
| def test_sw_rh(self): | ||
| modes = [[0, 3, -2, 0], [1, 1, 1, 1]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1, 1) | ||
| x = treams.efield(r, basis=b, k0=k0, poltype="helicity", material=material) | ||
| rsph = sc.car2sph(r[:, None] - positions) | ||
| y = sc.vsw_rA( | ||
| [3, 1], | ||
| [-2, 1], | ||
| k0 * rsph[..., 0] * [1, 3], | ||
| rsph[..., 1], | ||
| rsph[..., 2], | ||
| [0, 1], | ||
| ) | ||
| assert np.all(sc.vsph2car(y, rsph).swapaxes(-1, -2) == x) | ||
| def test_sw_sh(self): | ||
| modes = [[0, 3, -2, 0], [1, 1, 1, 1]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1, 1) | ||
| x = treams.efield( | ||
| r, | ||
| basis=b, | ||
| k0=k0, | ||
| poltype="helicity", | ||
| material=material, | ||
| modetype="singular", | ||
| ) | ||
| rsph = sc.car2sph(r[:, None] - positions) | ||
| y = sc.vsw_A( | ||
| [3, 1], | ||
| [-2, 1], | ||
| k0 * rsph[..., 0] * [1, 3], | ||
| rsph[..., 1], | ||
| rsph[..., 2], | ||
| [0, 1], | ||
| ) | ||
| assert np.all(sc.vsph2car(y, rsph).swapaxes(-1, -2) == x) | ||
| def test_sw_rp(self): | ||
| modes = [[0, 3, -2, 0], [1, 1, 1, 0]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1) | ||
| x = treams.efield(r, basis=b, k0=k0, poltype="parity", material=material) | ||
| rsph = sc.car2sph(r[:, None] - positions) | ||
| y = sc.vsw_rM( | ||
| [3, 1], [-2, 1], 2 * k0 * rsph[..., 0], rsph[..., 1], rsph[..., 2], | ||
| ) | ||
| assert np.all(sc.vsph2car(y, rsph).swapaxes(-1, -2) == x) | ||
| def test_sw_sp(self): | ||
| modes = [[0, 3, -2, 1], [1, 1, 1, 1]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.SphericalWaveBasis(modes, positions) | ||
| r = np.array([3, 4, 5]) | ||
| k0 = 4 | ||
| material = (4, 1) | ||
| x = treams.efield( | ||
| r, basis=b, k0=k0, poltype="parity", material=material, modetype="singular", | ||
| ) | ||
| rsph = sc.car2sph(r[None] - positions) | ||
| y = sc.vsw_N( | ||
| [3, 1], [-2, 1], 2 * k0 * rsph[..., 0], rsph[..., 1], rsph[..., 2], | ||
| ) | ||
| assert np.all(sc.vsph2car(y, rsph).swapaxes(-1, -2) == x) | ||
| def test_cw_rh(self): | ||
| modes = [[0, 0.3, -2, 0], [1, 0.1, 1, 1]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1, 1) | ||
| x = treams.efield(r, basis=b, k0=k0, poltype="helicity", material=material) | ||
| rcyl = sc.car2cyl(r[:, None] - positions) | ||
| y = sc.vcw_rA( | ||
| [0.3, 0.1], | ||
| [-2, 1], | ||
| rcyl[..., 0] * [np.sqrt(16 - 0.3 ** 2), np.sqrt(144 - 0.1 ** 2)], | ||
| rcyl[..., 1], | ||
| rcyl[..., 2], | ||
| [k0, 3 * k0], | ||
| [0, 1], | ||
| ) | ||
| assert np.all(np.abs(sc.vcyl2car(y, rcyl).swapaxes(-1, -2) - x) < 1e-14) | ||
| def test_cw_sh(self): | ||
| modes = [[0, 0.3, -2, 0], [1, 0.1, 1, 1]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1, 1) | ||
| x = treams.efield( | ||
| r, | ||
| basis=b, | ||
| k0=k0, | ||
| poltype="helicity", | ||
| material=material, | ||
| modetype="singular", | ||
| ) | ||
| rcyl = sc.car2cyl(r[:, None] - positions) | ||
| y = sc.vcw_A( | ||
| [0.3, 0.1], | ||
| [-2, 1], | ||
| rcyl[..., 0] * [np.sqrt(16 - 0.3 ** 2), np.sqrt(144 - 0.1 ** 2)], | ||
| rcyl[..., 1], | ||
| rcyl[..., 2], | ||
| [k0, 3 * k0], | ||
| [0, 1], | ||
| ) | ||
| assert np.all(sc.vcyl2car(y, rcyl).swapaxes(-1, -2) == x) | ||
| def test_cw_rp(self): | ||
| modes = [[0, 0.3, -2, 0], [1, 0.1, 1, 0]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1) | ||
| x = treams.efield(r, basis=b, k0=k0, poltype="parity", material=material) | ||
| rcyl = sc.car2cyl(r[:, None] - positions) | ||
| y = sc.vcw_rM( | ||
| [0.3, 0.1], | ||
| [-2, 1], | ||
| rcyl[..., 0] * [np.sqrt(64 - 0.3 ** 2), np.sqrt(64 - 0.1 ** 2)], | ||
| rcyl[..., 1], | ||
| rcyl[..., 2], | ||
| ) | ||
| assert np.all(np.abs(sc.vcyl2car(y, rcyl).swapaxes(-1, -2) - x) < 1e-14) | ||
| def test_cw_sp(self): | ||
| modes = [[0, 0.3, -2, 1], [1, 0.1, 1, 1]] | ||
| positions = np.array([[0, 0, 0], [1, 0, 0]]) | ||
| b = treams.CylindricalWaveBasis(modes, positions) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| k0 = 4 | ||
| material = (4, 1) | ||
| x = treams.efield( | ||
| r, basis=b, k0=k0, poltype="parity", material=material, modetype="singular", | ||
| ) | ||
| rcyl = sc.car2cyl(r[:, None] - positions) | ||
| y = sc.vcw_N( | ||
| [0.3, 0.1], | ||
| [-2, 1], | ||
| rcyl[..., 0] * [np.sqrt(64 - 0.3 ** 2), np.sqrt(64 - 0.1 ** 2)], | ||
| rcyl[..., 1], | ||
| rcyl[..., 2], | ||
| 8, | ||
| ) | ||
| assert np.all(sc.vcyl2car(y, rcyl).swapaxes(-1, -2) == x) | ||
| def test_pw_h(self): | ||
| modes = [[0, 3, 4, 0], [0, 4, 3, 1]] | ||
| b = treams.PlaneWaveBasisByUnitVector(modes) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| x = treams.efield(r, basis=b, k0=5, poltype="helicity") | ||
| y = sc.vpw_A( | ||
| [0, 0], | ||
| [3, 4], | ||
| [4, 3], | ||
| r[..., None, 0], | ||
| r[..., None, 1], | ||
| r[..., None, 2], | ||
| [0, 1], | ||
| ).swapaxes(-1, -2) | ||
| assert np.all(y == x) | ||
| def test_pw_p(self): | ||
| modes = [[0, 3, 4, 1], [0, 4, 3, 1]] | ||
| b = treams.PlaneWaveBasisByUnitVector(modes) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| x = treams.efield(r, basis=b, k0=5, poltype="parity") | ||
| y = sc.vpw_N( | ||
| [0, 0], [3, 4], [4, 3], r[..., None, 0], r[..., None, 1], r[..., None, 2] | ||
| ).swapaxes(-1, -2) | ||
| assert np.all(y == x) | ||
| def test_pwp_h(self): | ||
| modes = [[0, 3, 0], [0, 4, 1]] | ||
| b = treams.PlaneWaveBasisByComp(modes) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| x = treams.efield(r, basis=b, k0=5, poltype="helicity") | ||
| y = sc.vpw_A( | ||
| [0, 0], | ||
| [3, 4], | ||
| [4, 3], | ||
| r[..., None, 0], | ||
| r[..., None, 1], | ||
| r[..., None, 2], | ||
| [0, 1], | ||
| ).swapaxes(-1, -2) | ||
| assert np.all(y == x) | ||
| def test_pwp_p(self): | ||
| modes = [[0, 3, 1], [0, 4, 1]] | ||
| b = treams.PlaneWaveBasisByComp(modes) | ||
| r = np.array([[0, 1, 2], [3, 4, 5]]) | ||
| x = treams.efield( | ||
| r, basis=b, k0=1, poltype="parity", material=(5, 5), modetype="down" | ||
| ) | ||
| y = sc.vpw_N( | ||
| [0, 0], [3, 4], [-4, -3], r[..., None, 0], r[..., None, 1], r[..., None, 2] | ||
| ).swapaxes(-1, -2) | ||
| assert np.all(y == x) |
| import numpy as np | ||
| from treams import pw | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestToCw: | ||
| def test(self): | ||
| assert isclose(pw.to_cw(1, 4, 0, 3, 2, 1, 0), np.power((2 + 3j), 4) / 169) | ||
| def test_complex(self): | ||
| assert isclose(pw.to_cw(1, 4, 0, 3, 2j, 1, 0), 25) | ||
| def test_one(self): | ||
| assert pw.to_cw(1, 0, 0, 3, 2, 1, 0) == 1 | ||
| def test_pol(self): | ||
| assert pw.to_cw(5, 4, 1, 3, 2, 1, 0) == 0 | ||
| class TestToSw: | ||
| def test_h(self): | ||
| assert isclose( | ||
| pw.to_sw(5, 4, 0, 3, 2, 1, 0), -3.6625822219731057 + 3.632060703456664j | ||
| ) | ||
| def test_h_zero(self): | ||
| assert pw.to_sw(5, 4, 1, 3, 2, 1j, 0) == 0 | ||
| def test_p_same(self): | ||
| assert isclose( | ||
| pw.to_sw(5, 4, 0, 3, 2, 1, 0, poltype="parity"), | ||
| -1.3753807114589616 + 1.3639192055301372j, | ||
| ) | ||
| def test_p_opposite(self): | ||
| assert isclose( | ||
| pw.to_sw(5, 4, 1, 3, 2, 1j, 0, poltype="parity"), | ||
| 3.087192594399995 + 3.1131353893109184j, | ||
| ) | ||
| class TestTranslate: | ||
| def test(self): | ||
| assert pw.translate(1, 2, 3, 4, 5, 6) == np.exp(32j) | ||
| class TestPermuteXyz: | ||
| def test_h(self): | ||
| assert isclose( | ||
| pw.permute_xyz(1, 2, 3, 0, 0), (-6 + 1j * np.sqrt(14)) / np.sqrt(50) | ||
| ) | ||
| def test_h_inv(self): | ||
| assert isclose( | ||
| pw.permute_xyz(1, 2, 1j, 0, 0, inverse=True), -1j * np.sqrt(5 / 3) | ||
| ) | ||
| def test_h_opposite(self): | ||
| assert pw.permute_xyz(1, 2, 3, 0, 1) == 0 | ||
| def test_h_kxy_zero(self): | ||
| assert pw.permute_xyz(0, 0, -1, 0, 0) == 1j | ||
| def test_p(self): | ||
| assert isclose( | ||
| pw.permute_xyz(1, 2, 3, 0, 0, poltype="parity"), -6 / np.sqrt(50) | ||
| ) | ||
| def test_p_inv(self): | ||
| assert pw.permute_xyz( | ||
| 1, 2, 1j, 0, 0, poltype="parity", inverse=True | ||
| ) == -1j / np.sqrt(15) | ||
| def test_p_opposite(self): | ||
| assert pw.permute_xyz(1, 2, 3, 0, 1, poltype="parity") == -1j * np.sqrt( | ||
| 14 | ||
| ) / np.sqrt(50) | ||
| def test_p_inv_opposite(self): | ||
| assert pw.permute_xyz( | ||
| 1, 2, 1j, 0, 1, poltype="parity", inverse=True | ||
| ) == 4j / np.sqrt(15) | ||
| def test_p_kxy_zero(self): | ||
| assert pw.permute_xyz(0, 0, -1, 0, 0, poltype="parity") == 0 | ||
| def test_p_kxy_zero_opposite(self): | ||
| assert pw.permute_xyz(0, 0, -1, 0, 1, poltype="parity") == -1j |
| import numpy as np | ||
| import treams | ||
| from treams import SMatrices | ||
| def test_init(): | ||
| b = treams.PlaneWaveBasisByComp.default([0, 0]) | ||
| sm = SMatrices(np.zeros((2, 2, 2, 2)), basis=b, k0=1) | ||
| assert (sm["up", "down"] == np.zeros((2, 2))).all() | ||
| def test_interface(): | ||
| b = treams.PlaneWaveBasisByComp.default([1, 2]) | ||
| sm = SMatrices.interface(b, 2, [(2, 2, 1), (9, 1, 2)]) | ||
| m = treams.coeffs.fresnel( | ||
| [[2, 6], [2, 10]], [[1j, np.sqrt(31)], [1j, np.sqrt(95)]], [1, 1 / 3] | ||
| ) | ||
| assert ( | ||
| (sm[0, 0] == m[0, 0, ::-1, ::-1]).all() | ||
| and (sm[0, 1] == m[0, 1, ::-1, ::-1]).all() | ||
| and (sm[1, 0] == m[1, 0, ::-1, ::-1]).all() | ||
| and (sm[1, 1] == m[1, 1, ::-1, ::-1]).all() | ||
| and sm.k0 == 2 | ||
| and sm.basis == b | ||
| and sm.material == ((9, 1, 2), (2, 2, 1)) | ||
| ) | ||
| def test_slab(): | ||
| b = treams.PlaneWaveBasisByComp.default([1, 2]) | ||
| sm = SMatrices.slab(3, b, 6, [1, 2, 3]) | ||
| stest = SMatrices.interface(b, 6, [1, 2]) | ||
| stest = stest.add(SMatrices.propagation([0, 0, 3], b, 6, 2)) | ||
| stest = stest.add(SMatrices.interface(b, 6, [2, 3])) | ||
| assert sm == stest | ||
| class TestArray: | ||
| # a = np.eye(2) | ||
| # b = treams.lattice.reciprocal(a) | ||
| # kpars = [0.5, -1] + treams.lattice.diffr_orders_circle(b, 7) @ b | ||
| basis = treams.PlaneWaveBasisByComp.diffr_orders([0.5, -1], np.eye(2), 7) | ||
| expect = np.zeros((2, 2, 10, 10), complex) | ||
| expect[0, 0, :, :] = [ | ||
| [ | ||
| 0.993848113127513 + 0.0899002995446612j, | ||
| 0.000925536246960379 + 0.000428793418535671j, | ||
| 0.0648101516796695 + 0.0331042013001149j, | ||
| 0.0109997848957572 + 0.0879501663140503j, | ||
| -0.106944388611334 + 0.0220159623986055j, | ||
| -0.0524654995298520 - 0.0486312544338279j, | ||
| -0.0395282122841410 + 0.130130316412195j, | ||
| -0.0880140178152169 + 0.0622348131127568j, | ||
| -0.00776234664607976 - 0.0517964465530611j, | ||
| 0.0575011455260130 - 0.0136799639308035j, | ||
| ], | ||
| [ | ||
| 0.000368195704317514 + 0.000342748682178508j, | ||
| 0.993848113127513 + 0.0899002995446612j, | ||
| 0.0581175078289385 + 0.0374556467864508j, | ||
| 0.0345238953291475 - 0.0352425203792430j, | ||
| -0.0967629926372309 + 0.0135040926582279j, | ||
| -0.0908064879383120 + 0.102875938960868j, | ||
| 0.000499439658478980 - 0.0587987541584765j, | ||
| -0.0762544852103155 - 0.0277378127693338j, | ||
| -0.0335123168033311 + 0.0956716786902019j, | ||
| 0.0426603055441378 + 0.0935964180542818j, | ||
| ], | ||
| [ | ||
| -0.0224128384298332 - 0.0219558215375650j, | ||
| 0.0559328006698058 - 0.00699542481577819j, | ||
| 1.05772968849184 - 0.000726557597365671j, | ||
| 2.53274425719925e-05 - 0.00472287135244330j, | ||
| 0.0950097693048440 + 0.00349868895171054j, | ||
| 0.118914958142697 + 0.00899909281310485j, | ||
| 0.0131669242176258 - 0.00563755958120132j, | ||
| 0.0708970939412025 - 0.000381470646103337j, | ||
| 0.148214837500923 + 0.00710113570445585j, | ||
| 0.0889369170444238 + 0.00384425322789858j, | ||
| ], | ||
| [ | ||
| 0.0238202986243872 - 0.0369604188036947j, | ||
| 0.0210529527146176 - 0.0412166735686972j, | ||
| -0.000788697136521723 - 0.00370639325882959j, | ||
| 1.05772968849184 - 0.000726557597365654j, | ||
| 0.123848936004041 + 0.00901746586507995j, | ||
| 0.114317640119331 + 0.00673516839987441j, | ||
| 0.0955034726660238 + 0.00316322908150022j, | ||
| 0.167145666030970 + 0.00750611259077218j, | ||
| 0.0804645553554549 + 0.00233768107977424j, | ||
| 0.0160363331632666 - 0.00274273413656558j, | ||
| ], | ||
| [ | ||
| 0.0430316846420204 + 0.0379831882156353j, | ||
| -0.0203418294470036 + 0.0219456449502097j, | ||
| 0.0751896193624516 + 0.00442989155304419j, | ||
| 0.0782133923506291 + 0.00591893221832093j, | ||
| 1.03459593766718 - 0.00515702950143613j, | ||
| -0.00183503160716397 - 0.00808019295498379j, | ||
| 0.124150439267534 + 0.00578304089460873j, | ||
| 0.0515989281281407 - 0.00140448865784358j, | ||
| 0.00784420213394574 - 0.00521672677020705j, | ||
| 0.0618591931224174 + 0.000614358342229893j, | ||
| ], | ||
| [ | ||
| 0.00564858860599587 + 0.0404747176671437j, | ||
| 0.00920899445095448 + 0.0447334649038496j, | ||
| 0.0814585950765597 + 0.00593101664188928j, | ||
| 0.0624903416680794 + 0.00230117670616828j, | ||
| -0.00288578201244165 - 0.00811521904373132j, | ||
| 1.03459593766718 - 0.00515702950143615j, | ||
| 0.0560126542341310 - 0.00112183963492036j, | ||
| 0.00654258248991011 - 0.00663466702217734j, | ||
| 0.0470016864821933 - 0.000991905292032428j, | ||
| 0.118835847135738 + 0.00527095321442688j, | ||
| ], | ||
| [ | ||
| -0.0125246319270290 + 0.0344316752004385j, | ||
| 0.0281012830307396 + 0.0397415321359903j, | ||
| 0.118674655221182 + 0.00532939528085764j, | ||
| 0.0503374594115830 - 0.000270846971257591j, | ||
| 0.00706264585686750 - 0.00716205009690607j, | ||
| 0.0557004755423458 - 0.00151613006265248j, | ||
| 1.03862277988706 - 0.00446705555367979j, | ||
| -0.00219508632640696 - 0.00741309160956165j, | ||
| 0.0632478885667531 + 0.00199649718609005j, | ||
| 0.0884978111762212 + 0.00671330882080547j, | ||
| ], | ||
| [ | ||
| -0.0265497773644562 - 0.000225515181900920j, | ||
| 0.0587585736901527 + 0.0178484263957422j, | ||
| 0.0678081697252380 + 0.00224591596986489j, | ||
| 0.00934861327221400 - 0.00400270886750944j, | ||
| 0.0604650443412712 - 0.00121101355036169j, | ||
| 0.134019034054728 + 0.00624272905651435j, | ||
| -0.00109259930845912 - 0.00707642969716172j, | ||
| 1.03862277988706 - 0.00446705555367978j, | ||
| 0.0851592113836609 + 0.00670555592027250j, | ||
| 0.0892197306191960 + 0.00666247656250550j, | ||
| ], | ||
| [ | ||
| 0.0516543462992672 - 0.0235435312762873j, | ||
| -0.00754975039571915 - 0.0317339503514190j, | ||
| 0.0139162688635973 - 0.00238013424123935j, | ||
| 0.0771791179996622 + 0.00333602831486019j, | ||
| 0.156790798261392 + 0.00695444162689790j, | ||
| 0.0816163851501056 + 0.000810578097589877j, | ||
| 0.109047519088116 + 0.00814311515044666j, | ||
| 0.108165163540845 + 0.00820524413939098j, | ||
| 1.04961445458457 - 0.00230362147631233j, | ||
| -0.000860293278980379 - 0.00459476766940031j, | ||
| ], | ||
| [ | ||
| 0.0527996490125280 + 0.0184949045426726j, | ||
| -0.0285856194386801 + 0.00428391331727052j, | ||
| 0.0698268347829847 + 0.00202863074072325j, | ||
| 0.128620271681676 + 0.00616233852801594j, | ||
| 0.0620135432262052 - 0.00130870967208923j, | ||
| 0.0103495598672365 - 0.00688289581238961j, | ||
| 0.104084608465414 + 0.00819576826939015j, | ||
| 0.0773038125972791 + 0.00244018966991458j, | ||
| -0.000170308595664239 - 0.00584860919902162j, | ||
| 1.04961445458457 - 0.00230362147631235j, | ||
| ], | ||
| ] | ||
| expect[0, 1, :, :] = [ | ||
| [ | ||
| -0.00121078149386474 + 0.0105360703601872j, | ||
| 0.00574422068756450 - 0.0635401672310800j, | ||
| -0.0253030666878696 - 0.0386545650920068j, | ||
| -0.0596653988558001 + 0.0285699966014414j, | ||
| 0.0771472407627700 + 0.113260839302541j, | ||
| 0.0965915305871392 + 0.0280326390510608j, | ||
| 0.0828503474463993 - 0.0159811851477396j, | ||
| 0.0137058856410130 - 0.0562231823657133j, | ||
| -0.0528842309708006 + 0.0863036360333729j, | ||
| 0.0204951609529228 + 0.0986436718619743j, | ||
| ], | ||
| [ | ||
| 0.00574422068756448 - 0.0635401672310800j, | ||
| -0.000661251301840379 + 0.0106219091307033j, | ||
| -0.0220649981336179 + 0.0847439476373432j, | ||
| -0.0655632564494079 + 0.0241213868563773j, | ||
| 0.0637189572477666 - 0.0386451330452704j, | ||
| 0.105244474539718 + 0.0366893023741864j, | ||
| 0.0796899264978385 + 0.0740574570592570j, | ||
| 0.0225564259673991 + 0.133081869952488j, | ||
| -0.0499871371768027 - 0.0210649741895254j, | ||
| 0.0190341862946426 - 0.0490341097345966j, | ||
| ], | ||
| [ | ||
| -0.0245827628858486 + 0.0160917420023266j, | ||
| 0.0538937733698720 + 0.0140324594495978j, | ||
| 0.231438229589043 + 0.00638916820920929j, | ||
| 0.145090620819696 + 0.00260965627664133j, | ||
| 0.00507997677212323 - 0.00719456856856990j, | ||
| 0.0601752768619921 - 0.00184342131048677j, | ||
| 0.140879648047038 + 0.00187268530683326j, | ||
| 0.217488706951855 + 0.00817409029398223j, | ||
| 0.0688108517057143 - 0.00161422199257952j, | ||
| 0.000990173849110084 - 0.00574797601967016j, | ||
| ], | ||
| [ | ||
| 0.0181693792293622 + 0.0379448158081856j, | ||
| 0.0153402407233594 + 0.0416956181885270j, | ||
| 0.145090620819696 + 0.00260965627664134j, | ||
| 0.231960412306503 + 0.00614763611638012j, | ||
| 0.0324681584860382 - 0.00410554038048649j, | ||
| -0.00257352651987766 - 0.00709017151818118j, | ||
| 0.0104660368480857 - 0.00570865503676905j, | ||
| 0.101081926273698 - 0.000356171120013366j, | ||
| 0.166327790978530 + 0.00561205420255027j, | ||
| 0.0818267504441693 - 0.000450667795838653j, | ||
| ], | ||
| [ | ||
| 0.0473755551432816 - 0.0322697004667758j, | ||
| -0.0161647630626784 - 0.0266528218522238j, | ||
| 0.00334122992276013 - 0.00473205072798172j, | ||
| 0.0213551336033234 - 0.00270031832500847j, | ||
| 0.392210524336285 + 0.0100974878590117j, | ||
| 0.305491103403305 + 0.00640630969791261j, | ||
| 0.171259614900905 + 0.00260580831465352j, | ||
| 0.0484512190966304 - 0.00405008024594859j, | ||
| 0.105334792170552 + 0.00140697466471644j, | ||
| 0.186818806541043 + 0.00662420664576758j, | ||
| ], | ||
| [ | ||
| 0.0117256930581958 - 0.0404029972926602j, | ||
| 0.0153466642000930 - 0.0440224126696036j, | ||
| 0.0395788100380689 - 0.00121246507989009j, | ||
| -0.00169267384497088 - 0.00466338613279675j, | ||
| 0.305491103403305 + 0.00640630969791258j, | ||
| 0.393597649339270 + 0.00920346768214949j, | ||
| 0.242005684945980 + 0.00762234046944796j, | ||
| 0.162757694766738 + 0.00277205637477603j, | ||
| 0.0226054595075323 - 0.00377236165510020j, | ||
| 0.131272365173277 + 0.00191860356292815j, | ||
| ], | ||
| [ | ||
| -0.00721608669716130 - 0.0374099470431149j, | ||
| 0.0334396370338332 - 0.0359829019677023j, | ||
| 0.100025588797294 + 0.00132962037487127j, | ||
| 0.00743096332661488 - 0.00405318716513839j, | ||
| 0.184872871147433 + 0.00281294142269681j, | ||
| 0.261242475850786 + 0.00822823272296570j, | ||
| 0.347693111910818 + 0.00838917180126063j, | ||
| 0.261515262046721 + 0.00606363984315974j, | ||
| 0.000335269023654312 - 0.00520749638947393j, | ||
| 0.0535169888222304 - 0.00104565119232894j, | ||
| ], | ||
| [ | ||
| -0.0253868129672900 - 0.00618870616494402j, | ||
| 0.0600913075294249 - 0.0101850472198475j, | ||
| 0.154418585446480 + 0.00580366437503330j, | ||
| 0.0717689129157641 - 0.000252884121204149j, | ||
| 0.0523025582544394 - 0.00437201709159085j, | ||
| 0.175695141848094 + 0.00299240437556698j, | ||
| 0.261515262046721 + 0.00606363984315973j, | ||
| 0.345851664065581 + 0.00982597279438987j, | ||
| 0.0147765521941797 - 0.00429933303722608j, | ||
| 0.00559783899725002 - 0.00529131427737523j, | ||
| ], | ||
| [ | ||
| 0.0476295780888575 + 0.0291859500301806j, | ||
| -0.0116254178759258 + 0.0275870909156167j, | ||
| 0.0597137951251432 - 0.00140081569929742j, | ||
| 0.144338623747696 + 0.00487011927007519j, | ||
| 0.138977644769547 + 0.00185634794661291j, | ||
| 0.0298254114955998 - 0.00497721528890375j, | ||
| 0.000409777691580894 - 0.00636478677968881j, | ||
| 0.0180604261665935 - 0.00525479732105590j, | ||
| 0.265840432074685 + 0.00752251297923123j, | ||
| 0.181591899527160 + 0.00422135498979611j, | ||
| ], | ||
| [ | ||
| 0.0544398438798653 - 0.0113109471850463j, | ||
| -0.0270611305150395 - 0.0105046589477177j, | ||
| 0.000859269096347043 - 0.00498807170547375j, | ||
| 0.0710089424946557 - 0.000391087797391372j, | ||
| 0.246486817856847 + 0.00873991032901030j, | ||
| 0.173199412645827 + 0.00253138586907244j, | ||
| 0.0654103618070671 - 0.00127803197301369j, | ||
| 0.00684187735905910 - 0.00646723197502180j, | ||
| 0.181591899527160 + 0.00422135498979610j, | ||
| 0.265817226111122 + 0.00715697837137751j, | ||
| ], | ||
| ] | ||
| expect[1, 0, :, :] = [ | ||
| [ | ||
| -0.000661251301840380 + 0.0106219091307033j, | ||
| 0.00574422068756449 - 0.0635401672310800j, | ||
| -0.0655632564494079 + 0.0241213868563774j, | ||
| -0.0220649981336179 + 0.0847439476373432j, | ||
| 0.105244474539718 + 0.0366893023741863j, | ||
| 0.0637189572477666 - 0.0386451330452704j, | ||
| 0.0225564259673991 + 0.133081869952488j, | ||
| 0.0796899264978385 + 0.0740574570592570j, | ||
| 0.0190341862946426 - 0.0490341097345966j, | ||
| -0.0499871371768027 - 0.0210649741895254j, | ||
| ], | ||
| [ | ||
| 0.00574422068756448 - 0.0635401672310800j, | ||
| -0.00121078149386474 + 0.0105360703601872j, | ||
| -0.0596653988558001 + 0.0285699966014415j, | ||
| -0.0253030666878697 - 0.0386545650920068j, | ||
| 0.0965915305871392 + 0.0280326390510608j, | ||
| 0.0771472407627699 + 0.113260839302541j, | ||
| 0.0137058856410130 - 0.0562231823657133j, | ||
| 0.0828503474463992 - 0.0159811851477396j, | ||
| 0.0204951609529228 + 0.0986436718619743j, | ||
| -0.0528842309708005 + 0.0863036360333730j, | ||
| ], | ||
| [ | ||
| 0.0153402407233594 + 0.0416956181885270j, | ||
| 0.0181693792293622 + 0.0379448158081856j, | ||
| 0.231960412306503 + 0.00614763611638005j, | ||
| 0.145090620819696 + 0.00260965627664124j, | ||
| -0.00257352651987778 - 0.00709017151818119j, | ||
| 0.0324681584860382 - 0.00410554038048648j, | ||
| 0.101081926273698 - 0.000356171120013381j, | ||
| 0.0104660368480857 - 0.00570865503676909j, | ||
| 0.0818267504441693 - 0.000450667795838703j, | ||
| 0.166327790978530 + 0.00561205420255021j, | ||
| ], | ||
| [ | ||
| 0.0538937733698720 + 0.0140324594495977j, | ||
| -0.0245827628858486 + 0.0160917420023266j, | ||
| 0.145090620819696 + 0.00260965627664124j, | ||
| 0.231438229589043 + 0.00638916820920918j, | ||
| 0.0601752768619922 - 0.00184342131048682j, | ||
| 0.00507997677212339 - 0.00719456856856997j, | ||
| 0.217488706951855 + 0.00817409029398213j, | ||
| 0.140879648047038 + 0.00187268530683316j, | ||
| 0.000990173849110143 - 0.00574797601967020j, | ||
| 0.0688108517057144 - 0.00161422199257951j, | ||
| ], | ||
| [ | ||
| 0.0153466642000930 - 0.0440224126696036j, | ||
| 0.0117256930581958 - 0.0404029972926602j, | ||
| -0.00169267384497089 - 0.00466338613279674j, | ||
| 0.0395788100380688 - 0.00121246507989005j, | ||
| 0.393597649339270 + 0.00920346768214930j, | ||
| 0.305491103403305 + 0.00640630969791241j, | ||
| 0.162757694766738 + 0.00277205637477594j, | ||
| 0.242005684945980 + 0.00762234046944789j, | ||
| 0.131272365173277 + 0.00191860356292812j, | ||
| 0.0226054595075323 - 0.00377236165510023j, | ||
| ], | ||
| [ | ||
| -0.0161647630626783 - 0.0266528218522238j, | ||
| 0.0473755551432816 - 0.0322697004667758j, | ||
| 0.0213551336033234 - 0.00270031832500846j, | ||
| 0.00334122992276020 - 0.00473205072798169j, | ||
| 0.305491103403305 + 0.00640630969791246j, | ||
| 0.392210524336285 + 0.0100974878590115j, | ||
| 0.0484512190966304 - 0.00405008024594861j, | ||
| 0.171259614900905 + 0.00260580831465349j, | ||
| 0.186818806541043 + 0.00662420664576754j, | ||
| 0.105334792170552 + 0.00140697466471633j, | ||
| ], | ||
| [ | ||
| 0.0600913075294249 - 0.0101850472198475j, | ||
| -0.0253868129672900 - 0.00618870616494402j, | ||
| 0.0717689129157641 - 0.000252884121204201j, | ||
| 0.154418585446480 + 0.00580366437503326j, | ||
| 0.175695141848094 + 0.00299240437556694j, | ||
| 0.0523025582544393 - 0.00437201709159089j, | ||
| 0.345851664065581 + 0.00982597279438973j, | ||
| 0.261515262046721 + 0.00606363984315957j, | ||
| 0.00559783899724996 - 0.00529131427737522j, | ||
| 0.0147765521941796 - 0.00429933303722609j, | ||
| ], | ||
| [ | ||
| 0.0334396370338332 - 0.0359829019677023j, | ||
| -0.00721608669716131 - 0.0374099470431149j, | ||
| 0.00743096332661488 - 0.00405318716513840j, | ||
| 0.100025588797294 + 0.00132962037487128j, | ||
| 0.261242475850786 + 0.00822823272296553j, | ||
| 0.184872871147433 + 0.00281294142269667j, | ||
| 0.261515262046721 + 0.00606363984315961j, | ||
| 0.347693111910818 + 0.00838917180126050j, | ||
| 0.0535169888222303 - 0.00104565119232894j, | ||
| 0.000335269023654362 - 0.00520749638947396j, | ||
| ], | ||
| [ | ||
| -0.0270611305150396 - 0.0105046589477177j, | ||
| 0.0544398438798652 - 0.0113109471850463j, | ||
| 0.0710089424946556 - 0.000391087797391355j, | ||
| 0.000859269096346950 - 0.00498807170547373j, | ||
| 0.173199412645827 + 0.00253138586907232j, | ||
| 0.246486817856847 + 0.00873991032901021j, | ||
| 0.00684187735905901 - 0.00646723197502182j, | ||
| 0.0654103618070671 - 0.00127803197301367j, | ||
| 0.265817226111122 + 0.00715697837137738j, | ||
| 0.181591899527160 + 0.00422135498979600j, | ||
| ], | ||
| [ | ||
| -0.0116254178759258 + 0.0275870909156167j, | ||
| 0.0476295780888575 + 0.0291859500301807j, | ||
| 0.144338623747696 + 0.00487011927007516j, | ||
| 0.0597137951251432 - 0.00140081569929746j, | ||
| 0.0298254114955999 - 0.00497721528890372j, | ||
| 0.138977644769547 + 0.00185634794661296j, | ||
| 0.0180604261665935 - 0.00525479732105592j, | ||
| 0.000409777691580898 - 0.00636478677968882j, | ||
| 0.181591899527160 + 0.00422135498979598j, | ||
| 0.265840432074685 + 0.00752251297923111j, | ||
| ], | ||
| ] | ||
| expect[1, 1, :, :] = [ | ||
| [ | ||
| 0.993848113127513 + 0.0899002995446612j, | ||
| 0.000368195704317515 + 0.000342748682178508j, | ||
| 0.0345238953291475 - 0.0352425203792430j, | ||
| 0.0581175078289385 + 0.0374556467864508j, | ||
| -0.0908064879383120 + 0.102875938960868j, | ||
| -0.0967629926372310 + 0.0135040926582279j, | ||
| -0.0762544852103155 - 0.0277378127693338j, | ||
| 0.000499439658479015 - 0.0587987541584765j, | ||
| 0.0426603055441378 + 0.0935964180542817j, | ||
| -0.0335123168033311 + 0.0956716786902020j, | ||
| ], | ||
| [ | ||
| 0.000925536246960379 + 0.000428793418535676j, | ||
| 0.993848113127513 + 0.0899002995446612j, | ||
| 0.0109997848957571 + 0.0879501663140503j, | ||
| 0.0648101516796695 + 0.0331042013001149j, | ||
| -0.0524654995298519 - 0.0486312544338279j, | ||
| -0.106944388611334 + 0.0220159623986055j, | ||
| -0.0880140178152169 + 0.0622348131127568j, | ||
| -0.0395282122841410 + 0.130130316412195j, | ||
| 0.0575011455260130 - 0.0136799639308034j, | ||
| -0.00776234664607974 - 0.0517964465530611j, | ||
| ], | ||
| [ | ||
| 0.0210529527146176 - 0.0412166735686972j, | ||
| 0.0238202986243873 - 0.0369604188036947j, | ||
| 1.05772968849184 - 0.000726557597365666j, | ||
| -0.000788697136521705 - 0.00370639325882957j, | ||
| 0.114317640119331 + 0.00673516839987453j, | ||
| 0.123848936004041 + 0.00901746586508000j, | ||
| 0.167145666030970 + 0.00750611259077218j, | ||
| 0.0955034726660237 + 0.00316322908150025j, | ||
| 0.0160363331632666 - 0.00274273413656559j, | ||
| 0.0804645553554549 + 0.00233768107977422j, | ||
| ], | ||
| [ | ||
| 0.0559328006698059 - 0.00699542481577820j, | ||
| -0.0224128384298332 - 0.0219558215375650j, | ||
| 2.53274425721147e-05 - 0.00472287135244334j, | ||
| 1.05772968849184 - 0.000726557597365662j, | ||
| 0.118914958142697 + 0.00899909281310487j, | ||
| 0.0950097693048439 + 0.00349868895171065j, | ||
| 0.0708970939412025 - 0.000381470646103337j, | ||
| 0.0131669242176258 - 0.00563755958120129j, | ||
| 0.0889369170444238 + 0.00384425322789861j, | ||
| 0.148214837500923 + 0.00710113570445587j, | ||
| ], | ||
| [ | ||
| 0.00920899445095450 + 0.0447334649038496j, | ||
| 0.00564858860599586 + 0.0404747176671437j, | ||
| 0.0624903416680793 + 0.00230117670616833j, | ||
| 0.0814585950765597 + 0.00593101664188929j, | ||
| 1.03459593766718 - 0.00515702950143621j, | ||
| -0.00288578201244174 - 0.00811521904373130j, | ||
| 0.00654258248991009 - 0.00663466702217730j, | ||
| 0.0560126542341310 - 0.00112183963492035j, | ||
| 0.118835847135738 + 0.00527095321442691j, | ||
| 0.0470016864821934 - 0.000991905292032356j, | ||
| ], | ||
| [ | ||
| -0.0203418294470036 + 0.0219456449502097j, | ||
| 0.0430316846420204 + 0.0379831882156353j, | ||
| 0.0782133923506292 + 0.00591893221832097j, | ||
| 0.0751896193624516 + 0.00442989155304420j, | ||
| -0.00183503160716394 - 0.00808019295498380j, | ||
| 1.03459593766718 - 0.00515702950143617j, | ||
| 0.0515989281281407 - 0.00140448865784353j, | ||
| 0.124150439267534 + 0.00578304089460875j, | ||
| 0.0618591931224175 + 0.000614358342229880j, | ||
| 0.00784420213394577 - 0.00521672677020706j, | ||
| ], | ||
| [ | ||
| 0.0587585736901527 + 0.0178484263957421j, | ||
| -0.0265497773644562 - 0.000225515181900931j, | ||
| 0.00934861327221396 - 0.00400270886750944j, | ||
| 0.0678081697252379 + 0.00224591596986489j, | ||
| 0.134019034054728 + 0.00624272905651429j, | ||
| 0.0604650443412711 - 0.00121101355036160j, | ||
| 1.03862277988706 - 0.00446705555367977j, | ||
| -0.00109259930845908 - 0.00707642969716172j, | ||
| 0.0892197306191960 + 0.00666247656250551j, | ||
| 0.0851592113836610 + 0.00670555592027249j, | ||
| ], | ||
| [ | ||
| 0.0281012830307397 + 0.0397415321359903j, | ||
| -0.0125246319270290 + 0.0344316752004384j, | ||
| 0.0503374594115831 - 0.000270846971257560j, | ||
| 0.118674655221182 + 0.00532939528085768j, | ||
| 0.0557004755423458 - 0.00151613006265246j, | ||
| 0.00706264585686755 - 0.00716205009690603j, | ||
| -0.00219508632640699 - 0.00741309160956164j, | ||
| 1.03862277988706 - 0.00446705555367977j, | ||
| 0.0884978111762212 + 0.00671330882080548j, | ||
| 0.0632478885667531 + 0.00199649718609007j, | ||
| ], | ||
| [ | ||
| -0.0285856194386801 + 0.00428391331727053j, | ||
| 0.0527996490125280 + 0.0184949045426726j, | ||
| 0.128620271681676 + 0.00616233852801598j, | ||
| 0.0698268347829847 + 0.00202863074072327j, | ||
| 0.0103495598672364 - 0.00688289581238952j, | ||
| 0.0620135432262052 - 0.00130870967208924j, | ||
| 0.0773038125972792 + 0.00244018966991466j, | ||
| 0.104084608465414 + 0.00819576826939018j, | ||
| 1.04961445458457 - 0.00230362147631232j, | ||
| -0.000170308595664255 - 0.00584860919902158j, | ||
| ], | ||
| [ | ||
| -0.00754975039571915 - 0.0317339503514190j, | ||
| 0.0516543462992672 - 0.0235435312762873j, | ||
| 0.0771791179996623 + 0.00333602831486019j, | ||
| 0.0139162688635972 - 0.00238013424123936j, | ||
| 0.0816163851501056 + 0.000810578097589912j, | ||
| 0.156790798261392 + 0.00695444162689790j, | ||
| 0.108165163540845 + 0.00820524413939102j, | ||
| 0.109047519088116 + 0.00814311515044669j, | ||
| -0.000860293278980314 - 0.00459476766940031j, | ||
| 1.04961445458457 - 0.00230362147631232j, | ||
| ], | ||
| ] | ||
| def test(self): | ||
| tm = treams.TMatrix.sphere(4, 3, [0.2], [4, 1]).latticeinteraction.solve( | ||
| self.basis.lattice, self.basis.kpar[:2] | ||
| ) | ||
| sm = SMatrices.from_array(tm, self.basis) | ||
| assert all( | ||
| np.all(np.abs(sm[i, j] - self.expect[i, j]) < 1e-8) | ||
| for i in range(2) | ||
| for j in range(2) | ||
| ) | ||
| # def test_cyl(self): | ||
| # tm = treams.TMatrix.sphere(4, 3, [0.2], [4, 1]) | ||
| # # A larger range for kz (which later is kx) is needed for convergence | ||
| # cwb = treams.CylindricalWaveBasis.diffr_orders(0.5, 4, 1, 10) | ||
| # tmc = treams.TMatrixC.from_array(tm, cwb) | ||
| # basis = self.basis.permute() | ||
| # sm = SMatrices.from_array(tmc, basis) | ||
| # assert np.all(np.abs(sm - self.expect) < 1e-8) |
| import numpy as np | ||
| import pytest | ||
| import scipy.special as ssc | ||
| import treams.special as sc | ||
| import treams.special.cython_special as cs | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| EPS = 2e-7 | ||
| EPSSQ = 4e-14 | ||
| class TestIncgamma: | ||
| def test_zero_real(self): | ||
| assert isclose(sc.incgamma(0, 1.5), 0.10001958240663263, rel_tol=EPSSQ) | ||
| def test_zero_complex(self): | ||
| assert isclose( | ||
| sc.incgamma(0, 2 + 4j), | ||
| 0.006575211740584215 + 0.0261438237000811j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_zero_negreal(self): | ||
| assert isclose( | ||
| sc.incgamma(0, -3 + 0.0j), | ||
| -9.933832570625414 - 3.141592653589793j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_zero_negreal_branch(self): | ||
| assert isclose( | ||
| sc.incgamma(0, complex(-3, -0.0)), | ||
| -9.933832570625414 + 3.141592653589793j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_half_real(self): | ||
| assert isclose(sc.incgamma(0.5, 1.5), 0.1475825132040964, rel_tol=EPSSQ) | ||
| def test_half_complex(self): | ||
| assert isclose( | ||
| sc.incgamma(0.5, 2 + 4j), | ||
| -0.01415763494202471 + 0.058731665238669344j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_half_negreal(self): | ||
| assert isclose( | ||
| sc.incgamma(0.5, -3 + 0.0j), | ||
| 1.7724538509055152 - 14.626171384019093j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_half_negreal_branch(self): | ||
| assert isclose( | ||
| sc.incgamma(0.5, complex(-3, -0.0)), | ||
| 1.7724538509055152 + 14.626171384019093j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_one_real(self): | ||
| assert isclose(sc.incgamma(1, 1.5), 0.22313016014842982, rel_tol=EPSSQ) | ||
| def test_one_complex(self): | ||
| assert isclose( | ||
| sc.incgamma(1, 2 + 4j), | ||
| -0.08846104456538201 + 0.10242208005667372j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_one_negreal(self): | ||
| assert isclose(sc.incgamma(1, -3), 20.085536923187668, rel_tol=EPSSQ) | ||
| def test_one_negreal_branch(self): | ||
| assert isclose( | ||
| sc.incgamma(1, complex(-3, -0.0)), 20.085536923187668, rel_tol=EPSSQ | ||
| ) | ||
| def test_ten_real(self): | ||
| assert isclose(sc.incgamma(10, 1.5), 362878.5130988457, rel_tol=EPSSQ) | ||
| def test_ten_complex(self): | ||
| assert isclose( | ||
| sc.incgamma(10, 2 + 4j), | ||
| 345166.2033113096 - 45890.997544067905j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_ten_negreal(self): | ||
| assert isclose(sc.incgamma(10, -3), 270070.1294691813, rel_tol=EPSSQ) | ||
| def test_ten_negreal_branch(self): | ||
| assert isclose( | ||
| sc.incgamma(10, complex(-3, -0.0)), 270070.1294691813, rel_tol=EPSSQ | ||
| ) | ||
| def test_neg_real(self): | ||
| assert isclose(sc.incgamma(-10, 1.5), 0.0003324561166899859, rel_tol=EPSSQ) | ||
| def test_neg_complex(self): | ||
| assert isclose( | ||
| sc.incgamma(-10, 2 + 4j), | ||
| -3.109457703343637e-9 - 9.73849356067146e-10j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_neg_negreal(self): | ||
| assert isclose( | ||
| sc.incgamma(-10, -3 + 0.0j), | ||
| 0.00005342780082756921 - 8.657387162670285e-7j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_neg_negreal_branch(self): | ||
| assert isclose( | ||
| sc.incgamma(-10, complex(-3, -0.0)), | ||
| 0.00005342780082756921 + 8.657387162670285e-7j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_huge(self): | ||
| assert isclose( | ||
| sc.incgamma(100, 120 - 42j), | ||
| -4.4658781836612545e156 + 1.1325309029755172e155j, | ||
| rel_tol=EPS, | ||
| ) | ||
| def test_tiny(self): | ||
| assert isclose( | ||
| sc.incgamma(100, 1e-12 + 1e-3j), | ||
| 9.332621544394404e155 + 9.901988480274498e-306j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_huge_neg(self): | ||
| assert isclose( | ||
| sc.incgamma(-100, 42 - 12j), | ||
| -2.2632633469219943e-185 + 3.010799952817908e-185j, | ||
| rel_tol=EPS, | ||
| ) | ||
| def test_tiny_neg(self): | ||
| assert isclose( | ||
| sc.incgamma(-100, 1e-2 + 1e-2j), | ||
| -8.792072241883211e182 + 8.881173950746141e180j, | ||
| rel_tol=EPS, | ||
| ) | ||
| def test_huge_half(self): | ||
| assert isclose( | ||
| sc.incgamma(99.5, 120 - 42j), | ||
| -3.9035577320280324e155 - 5.255135904002826e154j, | ||
| rel_tol=EPSSQ, | ||
| ) | ||
| def test_tiny_half(self): | ||
| assert isclose( | ||
| sc.incgamma(99.5, 1e-8 + 1e-3j), | ||
| 9.367802114655592e154 + 2.249528564769392e-301j, | ||
| rel_tol=EPS, | ||
| ) | ||
| def test_huge_neg_half(self): | ||
| assert isclose( | ||
| sc.incgamma(-99.5, 42 - 12j), | ||
| -1.2102237586206635e-184 + 2.1853661691833773e-184j, | ||
| rel_tol=EPS, | ||
| ) | ||
| def test_tiny_neg_half(self): | ||
| assert isclose( | ||
| sc.incgamma(-99.5, 1e-2 + 1e-2j), | ||
| -9.748868790182211e181 - 3.9232159891109815e181j, | ||
| rel_tol=EPS, | ||
| ) | ||
| @pytest.mark.skip | ||
| def test_error(self): | ||
| with pytest.raises(ValueError): | ||
| sc.incgamma(0.4, 1) | ||
| def test_zero_zero(self): | ||
| result = sc.incgamma(0, 0) | ||
| assert np.isinf(result.real) and result.imag == 0 | ||
| def test_neg_zero(self): | ||
| result = sc.incgamma(-1, 0) | ||
| assert np.isinf(result.real) and result.imag == 0 | ||
| def test_gamma(self): | ||
| assert isclose(sc.incgamma(4, 0), 6) | ||
| class TestWigner3j: | ||
| def test_zeros(self): | ||
| assert isclose(sc.wigner3j(0, 0, 0, 0, 0, 0), 1) | ||
| def test_small(self): | ||
| assert isclose(sc.wigner3j(1, 0, 1, 0, 0, 0), -1 / np.sqrt(3)) | ||
| def test_medium(self): | ||
| assert isclose(sc.wigner3j(5, 2, 6, 3, -2, -1), np.sqrt(8 / 1001)) | ||
| def test_large(self): | ||
| assert isclose(sc.wigner3j(123, 60, 95, -64, 32, 32), -0.007933910368778899) | ||
| def test_large_forward(self): | ||
| assert isclose(sc.wigner3j(123, 60, 66, -64, 32, 32), -0.01069952664596778) | ||
| def test_more(self): | ||
| assert isclose(sc.wigner3j(43, 61, 21, -1, 12, -11), -0.02786405482897469) | ||
| def test_even_more(self): | ||
| assert isclose(sc.wigner3j(43, 61, 22, -9, -12, 21), -0.00005702392146902575) | ||
| @pytest.mark.skip | ||
| def test_error(self): | ||
| with pytest.raises(ValueError): | ||
| sc.wigner3j(-2, 1, 3, 0, 0, 0) | ||
| def test_triangular(self): | ||
| assert sc.wigner3j(3, 1, 1, 0, 0, 0) == 0.0 | ||
| def test_physical_jm(self): | ||
| assert sc.wigner3j(1, 1, 1, 0, -2, 2) == 0.0 | ||
| def test_physical_m(self): | ||
| assert sc.wigner3j(1, 1, 1, 0, 0, 1) == 0.0 | ||
| def test_zero_return(self): | ||
| assert sc.wigner3j(2, 2, 1, 0, 0, 0) == 0.0 | ||
| def test_divide_by_zero(self): | ||
| assert isclose(sc.wigner3j(4, 4, 1, 1, -1, 0), -1 / (6 * np.sqrt(5))) | ||
| def test_last_init(self): | ||
| assert isclose(sc.wigner3j(4, 7, 4, 1, 3, -4), np.sqrt(5 / 858)) | ||
| class TestHankel1: | ||
| def test(self): | ||
| assert sc.hankel1(1, 3 + 1j) == ssc.hankel1(1, 3 + 1j) | ||
| class TestHankel2: | ||
| def test(self): | ||
| assert sc.hankel2(1, 3 + 1j) == ssc.hankel2(1, 3 + 1j) | ||
| class TestJv: | ||
| def test_real_type(self): | ||
| assert isinstance(sc.jv(1, 3), float) | ||
| def test_real(self): | ||
| assert sc.jv(1, 3) == ssc.jv(1, 3) | ||
| def test_complex_type(self): | ||
| assert isinstance(sc.jv(1, 3 + 0j), complex) | ||
| def test_complex(self): | ||
| assert sc.jv(1, 3 + 1j) == ssc.jv(1, 3 + 1j) | ||
| class TestSpericalJn: | ||
| def test_real_type(self): | ||
| assert isinstance(sc.spherical_jn(1, 3), float) | ||
| def test_real(self): | ||
| assert sc.spherical_jn(1, 3) == ssc.spherical_jn(1, 3) | ||
| def test_complex_type(self): | ||
| assert isinstance(sc.spherical_jn(1, 3 + 0j), complex) | ||
| def test_complex(self): | ||
| assert sc.spherical_jn(1, 3 + 1j) == ssc.spherical_jn(1, 3 + 1j) | ||
| class TestSpericalYn: | ||
| def test_real_type(self): | ||
| assert isinstance(sc.spherical_yn(1, 3), float) | ||
| def test_real(self): | ||
| assert sc.spherical_yn(1, 3) == ssc.spherical_yn(1, 3) | ||
| def test_complex_type(self): | ||
| assert isinstance(sc.spherical_yn(1, 3 + 0j), complex) | ||
| def test_complex(self): | ||
| assert sc.spherical_yn(1, 3 + 1j) == ssc.spherical_yn(1, 3 + 1j) | ||
| class TestYv: | ||
| def test_real_type(self): | ||
| assert isinstance(sc.yv(1, 3), float) | ||
| def test_real(self): | ||
| assert sc.yv(1, 3) == ssc.yv(1, 3) | ||
| def test_complex_type(self): | ||
| assert isinstance(sc.yv(1, 3 + 0j), complex) | ||
| def test_complex(self): | ||
| assert sc.yv(1, 3 + 1j) == ssc.yv(1, 3 + 1j) | ||
| class TestLpmv: | ||
| def test_real_type(self): | ||
| assert isinstance(sc.lpmv(1, 2, 0.3), float) | ||
| def test_real(self): | ||
| assert sc.lpmv(1, 2, 0.3) == ssc.lpmv(1, 2, 0.3) | ||
| def test_complex_type(self): | ||
| assert isinstance(sc.lpmv(1, 2, 0.3 + 0j), complex) | ||
| @pytest.mark.skip | ||
| def test_complex_error(self): | ||
| with pytest.raises(ValueError): | ||
| sc.lpmv(0, -1, 0j) | ||
| def test_non_physical(self): | ||
| assert sc.lpmv(2, 1, 0j) == 0 | ||
| def test_complex_00(self): | ||
| assert sc.lpmv(0, 0, 4j) == 1 | ||
| def test_complex_01(self): | ||
| assert sc.lpmv(0, 1, 3 + 0j) == 3 | ||
| def test_complex_even(self): | ||
| assert isclose( | ||
| sc.lpmv(0, 14, 1 / np.sqrt(2) + 0j), ssc.lpmv(0, 14, 1 / np.sqrt(2)) | ||
| ) | ||
| def test_complex_odd(self): | ||
| assert isclose( | ||
| sc.lpmv(0, 13, -1.2 + 0j), ssc.clpmn(0, 13, -1.2, type=2)[0][0, 13] | ||
| ) | ||
| def test_complex_odd_c(self): | ||
| assert isclose( | ||
| sc.lpmv(0, 13, -1.2 - 1j), ssc.clpmn(0, 13, -1.2 - 1j, type=2)[0][0, 13] | ||
| ) | ||
| def test_complex_asso(self): | ||
| assert isclose( | ||
| sc.lpmv(3, 3, 0.1 + 0j), ssc.clpmn(3, 3, -0.1 + 0j, type=2)[0][3, 3] | ||
| ) | ||
| def test_complex_asso_above(self): | ||
| assert isclose( | ||
| sc.lpmv(-5, 12, -1.3 + 0j), | ||
| ssc.clpmn(-5, 12, -1.3 + 1e-16j, type=2)[0][5, 12], | ||
| ) | ||
| def test_complex_asso_below(self): | ||
| assert isclose( | ||
| sc.lpmv(-5, 12, complex(-1.3, -0.0)), | ||
| ssc.clpmn(-5, 12, complex(-1.3, 0), type=2)[0][5, 12], | ||
| ) | ||
| def test_complex_asso_odd(self): | ||
| assert isclose( | ||
| sc.lpmv(-4, 13, 1.3 - 3j), ssc.clpmn(-4, 13, 1.3 - 3j, type=2)[0][4, 13] | ||
| ) | ||
| def test_complex_asso_pos(self): | ||
| assert isclose( | ||
| sc.lpmv(5, 12, 0.4 + 0j), ssc.clpmn(5, 12, 0.4 + 0j, type=2)[0][5, 12] | ||
| ) | ||
| def test_complex_asso_pos_c(self): | ||
| assert isclose( | ||
| sc.lpmv(5, 12, 1.3 - 1j), ssc.clpmn(5, 12, 1.3 - 1j, type=2)[0][5, 12] | ||
| ) | ||
| def test_nan_l(self): | ||
| assert np.isnan(sc.lpmv(0, 0.5, 1j)) | ||
| def test_nan_m(self): | ||
| assert np.isnan(sc.lpmv(0.5, 1, 1j)) | ||
| class TestSphHarm: | ||
| def test_real(self): | ||
| assert sc.sph_harm(1, 2, 3, 4) == ssc.sph_harm(1, 2, 3, 4) | ||
| def test_complex(self): | ||
| assert isclose( | ||
| sc.sph_harm(1, 2, 3, 4j), | ||
| np.sqrt(5 / (24 * np.pi)) | ||
| * ssc.clpmn(1, 2, np.cos(4j), type=2)[0][1, 2] | ||
| * np.exp(3j), | ||
| ) | ||
| def test_nan_l(self): | ||
| assert np.isnan(sc.sph_harm(0, 0.5, 0, 1j)) | ||
| def test_nan_m(self): | ||
| assert np.isnan(sc.sph_harm(0.5, 1, 0, 1j)) | ||
| class TestSphHankel1: | ||
| def test(self): | ||
| assert isclose( | ||
| sc.spherical_hankel1(1, 2 + 1j), 0.0589704984384257 - 0.1995739736279250j | ||
| ) | ||
| class TestSphHankel2: | ||
| def test(self): | ||
| assert isclose( | ||
| sc.spherical_hankel2(1, 2 + 1j), 1.0624415871431773 + 0.2312289984789762j | ||
| ) | ||
| class TestJvD: | ||
| def test_zero(self): | ||
| assert isclose(sc.jv_d(0, 3), ssc.jvp(0, 3)) | ||
| def test_non_zero(self): | ||
| assert isclose(sc.jv_d(2, 3 + 4j), ssc.jvp(2, 3 + 4j)) | ||
| class TestYvD: | ||
| def test_zero(self): | ||
| assert isclose(sc.yv_d(0, 3), ssc.yvp(0, 3)) | ||
| def test_non_zero(self): | ||
| assert isclose(sc.yv_d(2, 3 + 4j), ssc.yvp(2, 3 + 4j)) | ||
| class TestHankel1D: | ||
| def test_zero(self): | ||
| assert isclose(sc.hankel1_d(0, 3), ssc.h1vp(0, 3)) | ||
| def test_non_zero(self): | ||
| assert isclose(sc.hankel1_d(2, 3 + 4j), ssc.h1vp(2, 3 + 4j)) | ||
| class TestHankel2D: | ||
| def test_zero(self): | ||
| assert isclose(sc.hankel2_d(0, 3), ssc.h2vp(0, 3)) | ||
| def test_non_zero(self): | ||
| assert isclose(sc.hankel2_d(2, 3 + 4j), ssc.h2vp(2, 3 + 4j)) | ||
| class TestSphericalJnD: | ||
| def test_zero(self): | ||
| assert isclose(sc.spherical_jn_d(0, 3), ssc.spherical_jn(0, 3, True)) | ||
| def test_non_zero(self): | ||
| assert isclose(sc.spherical_jn_d(2, 3 + 4j), ssc.spherical_jn(2, 3 + 4j, True)) | ||
| def test_zero_arg(self): | ||
| assert isclose(sc.spherical_jn_d(3, 0), ssc.spherical_jn(3, 0, True)) | ||
| def test_zero_arg_2(self): | ||
| assert isclose(sc.spherical_jn_d(1, 0), ssc.spherical_jn(1, 0, True)) | ||
| class TestSphericalYnD: | ||
| def test_zero(self): | ||
| assert isclose(sc.spherical_yn_d(0, 3), ssc.spherical_yn(0, 3, True)) | ||
| def test_non_zero(self): | ||
| assert isclose(sc.spherical_yn_d(2, 3 + 4j), ssc.spherical_yn(2, 3 + 4j, True)) | ||
| class TestSphericalHankel1D: | ||
| def test_zero(self): | ||
| assert isclose( | ||
| sc.spherical_hankel1_d(0, 3), -0.3456774997623560 - 0.0629591636023160j | ||
| ) | ||
| def test_non_zero(self): | ||
| assert isclose( | ||
| sc.spherical_hankel1_d(2, 3 + 4j), | ||
| 0.005890898262660627 - 0.003935166138594885j, | ||
| ) | ||
| class TestSphericalHankel2D: | ||
| def test_zero(self): | ||
| assert isclose( | ||
| sc.spherical_hankel2_d(0, 3), -0.3456774997623560 + 0.0629591636023160j | ||
| ) | ||
| def test_non_zero(self): | ||
| assert isclose( | ||
| sc.spherical_hankel2_d(2, 3 + 4j), 2.193962713943190 - 5.312458889454144j | ||
| ) | ||
| class TestIntkambe: | ||
| def test_m3_1(self): | ||
| assert isclose(sc.intkambe(-3, 0.8, 1.2), 0.1536931539507005) | ||
| def test_m3_2(self): | ||
| assert isclose(sc.intkambe(-3, 0.3, 0.3), 256.2213077991314, rel_tol=1e-5) | ||
| def test_m3_3(self): | ||
| assert isclose(sc.intkambe(-3, 5, 4), 2.208658257800406e-91) | ||
| def test_m3_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(-3, 1 + 1j, 2 - 1.1j), | ||
| -0.00009809435152495933 - 0.00015139494926761342j, | ||
| ) | ||
| def test_m2_1(self): | ||
| assert isclose(sc.intkambe(-2, 0.8, 1.2), 0.2340296298080376) | ||
| def test_m2_2(self): | ||
| assert isclose(sc.intkambe(-2, 0.3, 0.3), 87.3244612483742, rel_tol=1e-5) | ||
| def test_m2_3(self): | ||
| assert isclose(sc.intkambe(-2, 5, 4), 8.8564460016641e-91) | ||
| def test_m2_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(-2, 1 + 1j, 2 - 1.1j), | ||
| -0.00039153718178222677 - 0.00019654173190154122j, | ||
| ) | ||
| def test_m1_1(self): | ||
| assert isclose(sc.intkambe(-1, 0.8, 1.2), 0.373562480945606) | ||
| def test_m1_2(self): | ||
| assert isclose(sc.intkambe(-1, 0.3, 0.3), 31.60318167885612, rel_tol=1e-5) | ||
| def test_m1_3(self): | ||
| assert isclose(sc.intkambe(-1, 5, 4), 3.55134657134584e-90) | ||
| def test_m1_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(-1, 1 + 1j, 2 - 1.1j), | ||
| -0.001064442613991556 + 0.00007284003446857335j, | ||
| ) | ||
| def test_0_1(self): | ||
| assert isclose(sc.intkambe(0, 0.8, 1.2), 0.6329973232058413) | ||
| def test_0_2(self): | ||
| assert isclose(sc.intkambe(0, 0.3, 0.3), 14.50548439475395) | ||
| def test_0_3(self): | ||
| assert isclose(sc.intkambe(0, 5, 4), 1.424063216552536e-89) | ||
| def test_0_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(0, 1 + 1j, 2 - 1.1j), | ||
| -0.002143348723888108 + 0.0014826182600382856j, | ||
| ) | ||
| def test_fodd_1(self): | ||
| assert isclose(sc.intkambe(9, 0.8, 1.2), 3722.587631513942) | ||
| def test_fodd_2(self): | ||
| assert isclose(sc.intkambe(9, 0.4, 0.4), 3.698976253172273e6, rel_tol=1e-5) | ||
| def test_fodd_r(self): | ||
| assert isclose(sc.intkambe(9, 5, 4), 3.818351328744268e-84) | ||
| def test_fodd_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(9, 1.4 + 0.01j, 3 - 0.01j), | ||
| 0.8489489550578334 - 0.09535673649786904j, | ||
| rel_tol=1e-7, | ||
| ) | ||
| def test_feven_1(self): | ||
| assert isclose(sc.intkambe(10, 0.8, 1.2), 14289.0024493579) | ||
| def test_feven_2(self): | ||
| assert isclose(sc.intkambe(10, 0.4, 0.4), 2.849030924828978e7) | ||
| def test_feven_3(self): | ||
| assert isclose(sc.intkambe(10, 5, 4), 1.53122554488494e-83) | ||
| def test_feven_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(10, 1.4 + 0.01j, 3 - 0.01j), | ||
| 2.7511557962581623 - 0.3203862171927444j, | ||
| rel_tol=1e-5, | ||
| ) | ||
| @pytest.mark.skip | ||
| def test_error(self): | ||
| with pytest.raises(ValueError): | ||
| sc.intkambe(0.3, 1, 1) | ||
| def test_inf(self): | ||
| assert np.isinf(sc.intkambe(1, 1, 0)) | ||
| def test_m3_zero(self): | ||
| assert isclose( | ||
| sc.intkambe(-3, 0, 1 + 1j), -0.031087578289355378 - 0.24740395925452308j | ||
| ) | ||
| def test_m2_zero(self): | ||
| assert isclose( | ||
| sc.intkambe(-2, 0, 1 + 1j), 0.4554030049462477 - 0.5383650534833417j | ||
| ) | ||
| def test_inf_2(self): | ||
| assert np.isinf(sc.intkambe(-1, 0, 1)) | ||
| def test_inf_3(self): | ||
| assert np.isinf(sc.intkambe(-1, 1, 0)) | ||
| def test_bodd_1(self): | ||
| assert isclose(sc.intkambe(-9, 0.8, 1.2), 0.021334939311674394170463156) | ||
| def test_bodd_2(self): | ||
| assert isclose(sc.intkambe(-9, 0.4, 0.4), 2552.5567756521395093849138) | ||
| def test_bodd_r(self): | ||
| assert isclose(sc.intkambe(-9, 3, 2), 1.471110396630751e-12) | ||
| def test_bodd_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(-9, 1.4 + 0.01j, 3 - 0.01j), | ||
| 8.77953e-10 - 3.97705e-11j, | ||
| rel_tol=1e-5, | ||
| ) | ||
| def test_beven_1(self): | ||
| assert isclose(sc.intkambe(-10, 0.8, 1.2), 0.0161459, rel_tol=1e-5) | ||
| def test_beven_2(self): | ||
| assert isclose(sc.intkambe(-10, 0.4, 0.4), 5915.31, rel_tol=1e-5) | ||
| def test_beven_3(self): | ||
| assert isclose(sc.intkambe(-10, 3, 2), 7.203365170219268e-13) | ||
| def test_beven_complex(self): | ||
| assert isclose( | ||
| sc.intkambe(-10, 1.4 + 0.01j, 3 - 0.01j), | ||
| 2.82715e-10 - 1.18144e-11j, | ||
| rel_tol=1e-5, | ||
| ) | ||
| def test_m4_zzero(self): | ||
| assert isclose(sc.intkambe(-4, 0, 3), 0.01276548573157296882) | ||
| def test_m6_negz(self): | ||
| assert isclose( | ||
| sc.intkambe(-6, -2 - 1j, 3), 1.20189537847431e-10 + 3.80731298663362e-12j | ||
| ) | ||
| def test_6_negz(self): | ||
| assert isclose(sc.intkambe(6, -1, 3), 4.959558569294379) | ||
| def test_m5_negz(self): | ||
| assert isclose( | ||
| sc.intkambe(-5, -2 + 1j, 3), 3.65682124179e-10 - 6.38779831117841e-12j | ||
| ) | ||
| class TestWignerD: | ||
| @pytest.mark.skip | ||
| def test_error(self): | ||
| with pytest.raises(ValueError): | ||
| sc.wignersmalld(-1, 0, 0, 0) | ||
| def test_warning(self): | ||
| assert sc.wignersmalld(1, 0, 2, 0) == 0 | ||
| def test_1(self): | ||
| assert sc.wignersmalld(1, 0, 0, 0) == 1 | ||
| def test_2(self): | ||
| assert sc.wignersmalld(1, 1, 0, 2 * np.pi) == 0 | ||
| def test_3(self): | ||
| assert sc.wignersmalld(2, 1, -1, np.pi) == -1 | ||
| def test_4(self): | ||
| assert sc.wignersmalld(2, 1, 0, 2 * np.pi) == 0 | ||
| def test_5(self): | ||
| assert isclose(sc.wignersmalld(4, 3, 2, 1), -0.07526360176530718) | ||
| def test_6(self): | ||
| assert isclose(sc.wignersmalld(16, 8, -4, 2), -0.06370185806824848) | ||
| def test_7(self): | ||
| assert isclose(sc.wignersmalld(15, -7, -4, 4), -0.3126274668164052) | ||
| def test_8(self): | ||
| assert isclose(sc.wignersmalld(2, -1, 1, 6), 0.05815816395893696) | ||
| def test_9(self): | ||
| assert isclose( | ||
| sc.wignerd(4, 2, 3, 1.0, 2.0, 3.0), | ||
| -0.0011756123083512 - 0.2656305961739311j, | ||
| ) | ||
| def test_9_complex(self): | ||
| assert isclose( | ||
| sc.wignerd(4, 2, 3, 1.0, 2j, 3.0), -250.9892374159303 + 1.1108134417492j | ||
| ) | ||
| def test_10(self): | ||
| assert isclose( | ||
| sc.wignersmalld(2, 0, -2, 7 + 1j), 0.14867417641112 + 1.1000641894940703j | ||
| ) | ||
| def test_11(self): | ||
| assert isclose(sc.wignersmalld(2, 0, -2, np.pi + 1e-18), 0) | ||
| class TestPiFun: | ||
| def test_1_m1_0(self): | ||
| assert sc.pi_fun(1, -1, 1) == -0.5 | ||
| def test_2_1_pi(self): | ||
| assert sc.pi_fun(3, 1, -1) == -6 | ||
| def test_3_3_0(self): | ||
| assert sc.pi_fun(3, 3, 1) == 0 | ||
| def test_4_2_complex(self): | ||
| assert isclose( | ||
| sc.pi_fun(4, 2, np.cos(1 + 1j)), 51.9022970181194 - 253.1942074716320j | ||
| ) | ||
| def test_nan_l(self): | ||
| assert np.isnan(sc.pi_fun(0.5, 0, 1j)) | ||
| def test_nan_m(self): | ||
| assert np.isnan(sc.pi_fun(1, 0.5, 1j)) | ||
| class TestTauFun: | ||
| def test_1_m1_0(self): | ||
| assert sc.tau_fun(1, -1, 1) == 0.5 | ||
| def test_2_1_pi(self): | ||
| assert isclose(sc.tau_fun(3, 3, np.cos(0.2)), -1.740723332980088) | ||
| def test_4_2_complex(self): | ||
| assert isclose( | ||
| sc.tau_fun(4, 2, np.cos(1 + 1j)), -568.1643037179911 - 456.9245589672897j | ||
| ) | ||
| def test_nan_l(self): | ||
| assert np.isnan(sc.tau_fun(0.5, 0, 1j)) | ||
| def test_nan_m(self): | ||
| assert np.isnan(sc.tau_fun(1, 0.5, 1j)) | ||
| class TestVshZ: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vsh_Z(0, 0, 0, 0), [1j * np.sqrt(0.25 / np.pi), 0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal( | ||
| sc.vsh_Z(4, 3, 2j, 1), [1j * sc.sph_harm(3, 4, 1, 2j), 0, 0] | ||
| ) | ||
| class TestVshX: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vsh_X(0, 0, 0, 0), [0, 0, 0]) | ||
| def test_1(self): | ||
| expect = ( | ||
| -1j | ||
| * np.sqrt(15 / (2 * 3 * 8 * np.pi)) | ||
| * np.exp(1j) | ||
| * np.array([0, 1j * np.cos(2j), -np.cos(4j)]) | ||
| ) | ||
| assert np.sum(np.abs(sc.vsh_X(2, 1, 2j, 1) - expect)) < EPS | ||
| class TestVshY: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vsh_Y(0, 0, 0, 0), [0, 0, 0]) | ||
| def test_1(self): | ||
| expect = ( | ||
| -1j | ||
| * np.sqrt(15 / (2 * 3 * 8 * np.pi)) | ||
| * np.exp(1j) | ||
| * np.array([0, np.cos(4j), 1j * np.cos(2j)]) | ||
| ) | ||
| assert np.sum(np.abs(sc.vsh_Y(2, 1, 2j, 1) - expect)) < EPS | ||
| class TestVswM: | ||
| def test_0(self): | ||
| expect = ( | ||
| -1j | ||
| * (ssc.spherical_jn(2, 4 + 1j) + 1j * ssc.spherical_yn(2, 4 + 1j)) | ||
| * np.sqrt(15 / (2 * 3 * 8 * np.pi)) | ||
| * np.exp(1j) | ||
| * np.array([0, 1j * np.cos(2), -np.cos(4)]) | ||
| ) | ||
| assert np.sum(np.abs(sc.vsw_M(2, 1, 4 + 1j, 2, 1) - expect)) < EPS | ||
| class TestVswrM: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vsw_rM(1, 0, 0, 0, 0), [0, 0, 0]) | ||
| def test_1(self): | ||
| expect = ( | ||
| -1j | ||
| * ssc.spherical_jn(2, 4 + 1j) | ||
| * np.sqrt(15 / (2 * 3 * 8 * np.pi)) | ||
| * np.exp(1j) | ||
| * np.array([0, 1j * np.cos(2), -np.cos(4)]) | ||
| ) | ||
| assert np.sum(np.abs(sc.vsw_rM(2, 1, 4 + 1j, 2, 1) - expect)) < EPS | ||
| class TestVswN: | ||
| def test_0(self): | ||
| expect = ( | ||
| -1j | ||
| * np.sqrt(5 / (16 * np.pi)) | ||
| * np.exp(1j) | ||
| * np.array( | ||
| [ | ||
| 6 | ||
| * sc.spherical_hankel1(2, 4 + 1j) | ||
| / (4 + 1j) | ||
| * np.sin(2) | ||
| * np.cos(2), | ||
| np.cos(4) | ||
| * ( | ||
| sc.spherical_hankel1_d(2, 4 + 1j) | ||
| + sc.spherical_hankel1(2, 4 + 1j) / (4 + 1j) | ||
| ), | ||
| 1j | ||
| * np.cos(2) | ||
| * ( | ||
| sc.spherical_hankel1_d(2, 4 + 1j) | ||
| + sc.spherical_hankel1(2, 4 + 1j) / (4 + 1j) | ||
| ), | ||
| ] | ||
| ) | ||
| ) | ||
| assert np.sum(np.abs(sc.vsw_N(2, 1, 4 + 1j, 2, 1) - expect)) < EPS | ||
| class TestVswrN: | ||
| def test_0(self): | ||
| assert ( | ||
| np.sum( | ||
| np.abs( | ||
| sc.vsw_rN(1, 0, 0, 1, 0) | ||
| - [ | ||
| 1j / np.sqrt(6 * np.pi) * np.cos(1), | ||
| -1j / np.sqrt(6 * np.pi) * np.sin(1), | ||
| 0, | ||
| ] | ||
| ) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| def test_1(self): | ||
| expect = ( | ||
| -1j | ||
| * np.sqrt(5 / (16 * np.pi)) | ||
| * np.exp(1j) | ||
| * np.array( | ||
| [ | ||
| 6 * ssc.spherical_jn(2, 4 + 1j) / (4 + 1j) * np.sin(2) * np.cos(2), | ||
| np.cos(4) | ||
| * ( | ||
| ssc.spherical_jn(2, 4 + 1j, 1) | ||
| + ssc.spherical_jn(2, 4 + 1j) / (4 + 1j) | ||
| ), | ||
| 1j | ||
| * np.cos(2) | ||
| * ( | ||
| ssc.spherical_jn(2, 4 + 1j, 1) | ||
| + ssc.spherical_jn(2, 4 + 1j) / (4 + 1j) | ||
| ), | ||
| ] | ||
| ) | ||
| ) | ||
| assert np.sum(np.abs(sc.vsw_rN(2, 1, 4 + 1j, 2, 1) - expect)) < EPS | ||
| class TestVswA: | ||
| def test_p(self): | ||
| assert np.array_equal( | ||
| sc.vsw_A(5, 4, 3, 2, 1, 1), | ||
| (sc.vsw_N(5, 4, 3, 2, 1) + sc.vsw_M(5, 4, 3, 2, 1)) * np.sqrt(0.5), | ||
| ) | ||
| def test_m(self): | ||
| assert np.array_equal( | ||
| sc.vsw_A(5, 4, 3, 2, 1, 0), | ||
| (sc.vsw_N(5, 4, 3, 2, 1) - sc.vsw_M(5, 4, 3, 2, 1)) * np.sqrt(0.5), | ||
| ) | ||
| class TestVswrA: | ||
| def test_p(self): | ||
| assert np.array_equal( | ||
| sc.vsw_rA(5, 4, 3, 2, 1, 1), | ||
| (sc.vsw_rN(5, 4, 3, 2, 1) + sc.vsw_rM(5, 4, 3, 2, 1)) * np.sqrt(0.5), | ||
| ) | ||
| def test_m(self): | ||
| assert np.array_equal( | ||
| sc.vsw_rA(5, 4, 3j, 2, 1, 0), | ||
| (sc.vsw_rN(5, 4, 3j, 2, 1) - sc.vsw_rM(5, 4, 3j, 2, 1)) * np.sqrt(0.5), | ||
| ) | ||
| class TestTlVswA: | ||
| def test_0(self): | ||
| assert isclose( | ||
| sc.tl_vsw_A(1, 0, 1, 0, 1, 0, 0), 0.903506036819270 - 4.145319872028107j | ||
| ) | ||
| def test_1(self): | ||
| assert isclose( | ||
| sc.tl_vsw_A(14, 13, 11, -3, 12, 2, 1), | ||
| -6.265680341371548e02 - 2.084168034177037e03j, | ||
| ) | ||
| class TestTlVswrA: | ||
| def test_0(self): | ||
| assert isclose(sc.tl_vsw_rA(1, 0, 1, 0, 1, 0, 0), 0.903506036819270) | ||
| def test_1(self): | ||
| assert isclose( | ||
| sc.tl_vsw_rA(14, 13, 11, -3, 12 + 0j, 2, 1), | ||
| 6.853762595651612e-05 - 2.060462015430312e-05j, | ||
| ) | ||
| class TestTlVswB: | ||
| def test_0(self): | ||
| assert isclose(sc.tl_vsw_B(1, 0, 1, 0, 1, 0, 0), 0) | ||
| def test_1(self): | ||
| assert isclose( | ||
| sc.tl_vsw_B(14, 13, 11, -3, 12, 2, 1), | ||
| -2.087226903652359e02 + 6.274791216231195e01j, | ||
| ) | ||
| class TestTlVswrB: | ||
| def test_0(self): | ||
| assert isclose(sc.tl_vsw_rB(1, 0, 1, 0, 1, 0, 0), 0) | ||
| def test_1(self): | ||
| assert isclose( | ||
| sc.tl_vsw_rB(14, 13, 11, -3, 12, 2, 1), | ||
| -2.366181401015062e-04 - 7.870684079277587e-04j, | ||
| ) | ||
| class TestVcwM: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcw_M(0, 0, 1, 0, 0), [0, -sc.hankel1_d(0, 1), 0]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum( | ||
| np.abs( | ||
| sc.vcw_M(1, -2, 3, 4, 5) | ||
| - np.exp(-3j) | ||
| * np.array([-2j / 3 * sc.hankel1(-2, 3), -sc.hankel1_d(-2, 3), 0]) | ||
| ) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestVcwrM: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcw_rM(0, 0, 1, 0, 0), [0, -sc.jv_d(0, 1), 0]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum( | ||
| np.abs( | ||
| sc.vcw_rM(1, -2, 3, 4, 5) | ||
| - np.exp(-3j) | ||
| * np.array([-2j / 3 * sc.jv(-2, 3), -sc.jv_d(-2, 3), 0]) | ||
| ) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| def test_origin_m0(self): | ||
| assert np.array_equal(sc.vcw_rM(0, 0, 0, 0, 0), [0, 0, 0]) | ||
| def test_origin_m1(self): | ||
| assert np.array_equal(sc.vcw_rM(0, 1, 0, 0, 0), [0.5j, -0.5, 0]) | ||
| def test_origin_m4(self): | ||
| assert np.array_equal(sc.vcw_rM(0, 4, 0, 0, 0), [0, 0, 0]) | ||
| class TestVcwN: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcw_N(0, 0, 1, 0, 0, 1), [0, 0, sc.hankel1(0, 1)]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum( | ||
| np.abs( | ||
| sc.vcw_N(4, -2, 3, 1, 2, 5) | ||
| - np.exp(6j) | ||
| * np.array( | ||
| [ | ||
| 4j / 5 * sc.hankel1_d(-2, 3), | ||
| 2 * 4 / (3 * 5) * sc.hankel1(-2, 3), | ||
| 3 / 5 * sc.hankel1(-2, 3), | ||
| ] | ||
| ) | ||
| ) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestVcwrN: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcw_rN(0, 0, 1, 0, 0, 1), [0, 0, sc.jv(0, 1)]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum( | ||
| np.abs( | ||
| sc.vcw_rN(4, -2, 3, 1, 2, 5) | ||
| - np.exp(6j) | ||
| * np.array( | ||
| [ | ||
| 4j / 5 * sc.jv_d(-2, 3), | ||
| 2 * 4 / (3 * 5) * sc.jv(-2, 3), | ||
| 3 / 5 * sc.jv(-2, 3), | ||
| ] | ||
| ) | ||
| ) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestVcwA: | ||
| def test_p(self): | ||
| assert np.array_equal( | ||
| sc.vcw_A(4, -2, 3, 1, 2, 5, 1), | ||
| (sc.vcw_N(4, -2, 3, 1, 2, 5) + sc.vcw_M(4, -2, 3, 1, 2)) * np.sqrt(0.5), | ||
| ) | ||
| def test_m(self): | ||
| assert np.array_equal( | ||
| sc.vcw_A(4, -2, 3, 1, 2, 5, 0), | ||
| (sc.vcw_N(4, -2, 3, 1, 2, 5) - sc.vcw_M(4, -2, 3, 1, 2)) * np.sqrt(0.5), | ||
| ) | ||
| class TestVcwrA: | ||
| def test_p(self): | ||
| assert np.array_equal( | ||
| sc.vcw_rA(4, -2, 3, 1, 2, 5, 1), | ||
| (sc.vcw_rN(4, -2, 3, 1, 2, 5) + sc.vcw_rM(4, -2, 3, 1, 2)) * np.sqrt(0.5), | ||
| ) | ||
| def test_m(self): | ||
| assert np.array_equal( | ||
| sc.vcw_rA(4, -2, 3, 1, 2, 5, 0), | ||
| (sc.vcw_rN(4, -2, 3, 1, 2, 5) - sc.vcw_rM(4, -2, 3, 1, 2)) * np.sqrt(0.5), | ||
| ) | ||
| class TestTlVcw: | ||
| def test_0(self): | ||
| assert sc.tl_vcw(0, 0, 1, 0, 0, 0, 0) == 0 | ||
| def test_1(self): | ||
| assert sc.tl_vcw(1, 2, 1, 3, 4, 5, 6) == np.exp(11j) * sc.hankel1(1, 4) | ||
| class TestTlVcwr: | ||
| def test_0(self): | ||
| assert sc.tl_vcw_r(0, 0, 1, 0, 0, 0, 0) == 0 | ||
| def test_1(self): | ||
| assert sc.tl_vcw_r(1, 2, 1, 3, 4, 5, 6) == np.exp(11j) * sc.jv(1, 4) | ||
| class TestVpwM: | ||
| def test_k0(self): | ||
| res = sc.vpw_M(0, 0, 0j, 0, 0, 0) | ||
| assert np.all([np.isnan(i) for i in res]) | ||
| def test_kpar0(self): | ||
| assert np.array_equal(sc.vpw_M(0, 0, 3, 3, 2, 1), [0, -1j * np.exp(3j), 0]) | ||
| def test_k_general(self): | ||
| k = np.array([3, 4, -3]) | ||
| r = np.array([4, 0.2, 3]) | ||
| res = sc.vpw_M(*k, *r) | ||
| assert np.all( | ||
| np.abs( | ||
| res | ||
| - [ | ||
| 1j * k[1] * np.exp(1j * k @ r) / 5, | ||
| -1j * k[0] * np.exp(1j * k @ r) / 5, | ||
| 0, | ||
| ] | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestVpwN: | ||
| def test_k0(self): | ||
| res = sc.vpw_N(0, 0, 0j, 0, 0, 0) | ||
| assert np.all([np.isnan(i) for i in res]) | ||
| def test_kpar0(self): | ||
| assert np.array_equal(sc.vpw_N(0, 0, 3, 3, 2, 1), [-np.exp(3j), 0, 0]) | ||
| def test_kpar0_imagkz(self): | ||
| assert np.array_equal(sc.vpw_N(0, 0, -3j, 3, 2, 1), [np.exp(3), 0, 0]) | ||
| def test_k_general(self): | ||
| k = np.array([3, 4, -3]) | ||
| r = np.array([4, 0.2, 3]) | ||
| res = sc.vpw_N(*k, *r) | ||
| assert np.all( | ||
| np.abs( | ||
| res | ||
| - [ | ||
| -0.2441697182761140 - 0.1888789726889204j, | ||
| -0.3255596243681520 - 0.2518386302518939j, | ||
| -0.6782492174336500 - 0.5246638130247790j, | ||
| ] | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestVpwA: | ||
| def test(self): | ||
| k = np.array([-3, 1, -6]) | ||
| r = np.array([2, 0.2, 0.5]) | ||
| assert np.all( | ||
| np.abs( | ||
| np.sqrt(2) * sc.vpw_A(*k, *r, 0) - sc.vpw_N(*k, *r) + sc.vpw_M(*k, *r) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| def test_complex(self): | ||
| k = np.array([-3, 1j, -6]) | ||
| r = np.array([2, 0.2, 0.5]) | ||
| assert np.all( | ||
| np.abs( | ||
| np.sqrt(2) * sc.vpw_A(*k, *r, 0) - sc.vpw_N(*k, *r) + sc.vpw_M(*k, *r) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestCar2Cyl: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.car2cyl([0, 0, 0]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal(sc.car2cyl([3, -4, 1]), [5, -0.9272952180016122, 1]) | ||
| class TestCar2Sph: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.car2sph([0, 0, 0]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert np.all( | ||
| np.abs( | ||
| sc.car2sph([3, -4, 1]) | ||
| - [np.sqrt(26), np.arctan2(5, 1), -0.9272952180016122] | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestCyl2Car: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.cyl2car([0, 1, 0]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum(np.abs(sc.cyl2car([5, -0.9272952180016122, 1]) - [3, -4, 1])) < EPSSQ | ||
| ) | ||
| class TestCyl2Sph: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.cyl2sph([0, 1, 0]), [0, 0, 1]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum(np.abs(sc.cyl2sph([3, -4, 1]) - [np.sqrt(10), np.arctan2(3, 1), -4])) | ||
| < EPSSQ | ||
| ) | ||
| class TestSph2Car: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.sph2car([0, 1, 2]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum( | ||
| np.abs( | ||
| sc.sph2car([np.sqrt(26), np.arctan2(5, 1), -0.9272952180016122]) | ||
| - [3, -4, 1] | ||
| ) | ||
| ) | ||
| < EPSSQ | ||
| ) | ||
| class TestSph2Cyl: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.sph2cyl([0, 0, 1]), [0, 1, 0]) | ||
| def test_1(self): | ||
| assert ( | ||
| np.sum(np.abs(sc.sph2cyl([np.sqrt(10), np.arctan2(3, 1), -4]) - [3, -4, 1])) | ||
| < EPSSQ | ||
| ) | ||
| class TestCar2Pol: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.car2pol([0, 0]), [0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal(sc.car2pol([3, -4]), [5, -0.9272952180016122]) | ||
| class TestPol2Car: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.pol2car([0, 1]), [0, 0]) | ||
| def test_1(self): | ||
| assert np.sum(np.abs(sc.pol2car([5, -0.9272952180016122]) - [3, -4])) < EPSSQ | ||
| class TestVCarCyl: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcar2cyl([0, 0, 0j], [0, 0, 0]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal(sc.vcar2cyl([1, 0, 0], [0, 0, 0]), [1, 0, 0]) | ||
| def test_2(self): | ||
| assert np.array_equal(sc.vcyl2car([0, 0, 0], [0, 0, 0]), [0, 0, 0]) | ||
| def test_3(self): | ||
| assert np.array_equal(sc.vcyl2car([1, 0, 0], [0, 0, 0]), [1, 0, 0]) | ||
| def test_4(self): | ||
| pcar = [3, -4, 1] | ||
| vcar = [1, 2, 3] | ||
| pcyl = sc.car2cyl(pcar) | ||
| vcyl = sc.vcar2cyl(vcar, pcar) | ||
| assert np.sum(np.abs(sc.vcyl2car(vcyl, pcyl) - vcar)) < EPSSQ | ||
| class TestVCarSph: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcar2sph([0, 0, 0], [0, 0, 0]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal(sc.vcar2sph([1, 0, 0], [0, 0, 0]), [0, 1, 0]) | ||
| def test_2(self): | ||
| assert np.array_equal(sc.vsph2car([0, 0, 0], [0, 0, 0]), [0, 0, 0]) | ||
| def test_3(self): | ||
| assert np.array_equal(sc.vsph2car([0, 1, 0], [0, 0, 0]), [1, 0, 0]) | ||
| def test_4(self): | ||
| pcar = [3, -4, 1] | ||
| vcar = [1, 2, 3] | ||
| psph = sc.car2sph(pcar) | ||
| vsph = sc.vcar2sph(vcar, pcar) | ||
| assert np.sum(np.abs(sc.vsph2car(vsph, psph) - vcar)) < EPSSQ | ||
| class TestVCylSph: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcyl2sph([0, 0, 0], [0, 0, 0]), [0, 0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal(sc.vcyl2sph([1, 0, 0], [0, 0, 0]), [0, 1, 0]) | ||
| def test_2(self): | ||
| assert np.array_equal(sc.vsph2cyl([0, 0, 0], [0, 0, 0]), [0, 0, 0]) | ||
| def test_3(self): | ||
| assert np.array_equal(sc.vsph2cyl([0, 1, 0], [0, 0, 0]), [1, 0, 0]) | ||
| def test_4(self): | ||
| pcar = [3, -4, 1] | ||
| vcar = [1, 2, 3] | ||
| psph = sc.cyl2sph(pcar) | ||
| vsph = sc.vcyl2sph(vcar, pcar) | ||
| assert np.sum(np.abs(sc.vsph2cyl(vsph, psph) - vcar)) < EPSSQ | ||
| class TestVCarPol: | ||
| def test_0(self): | ||
| assert np.array_equal(sc.vcar2pol([0, 0], [0, 0]), [0, 0]) | ||
| def test_1(self): | ||
| assert np.array_equal(sc.vcar2pol([1, 0], [0, 0]), [1, 0]) | ||
| def test_2(self): | ||
| assert np.array_equal(sc.vpol2car([0, 0], [0, 0]), [0, 0]) | ||
| def test_3(self): | ||
| assert np.array_equal(sc.vpol2car([1, 0], [0, 0]), [1, 0]) | ||
| def test_4(self): | ||
| pcar = [3, -4] | ||
| vcar = [1, 2] | ||
| pcyl = sc.car2pol(pcar) | ||
| vcyl = sc.vcar2pol(vcar, pcar) | ||
| assert np.sum(np.abs(sc.vpol2car(vcyl, pcyl) - vcar)) < EPSSQ | ||
| class TestCythonSpecial: | ||
| def test_hankel1_d(self): | ||
| assert sc.hankel1_d(0.4, 0.3j) == cs.hankel1_d(0.4, 0.3j) | ||
| def test_hankel2_d(self): | ||
| assert sc.hankel2_d(0.4, 0.3j) == cs.hankel2_d(0.4, 0.3j) | ||
| def test_jv_d(self): | ||
| assert sc.jv_d(0.4, 0.3j) == cs.jv_d(0.4, 0.3j) | ||
| def test_spherical_hankel1(self): | ||
| assert sc.spherical_hankel1(4, 0.3j) == cs.spherical_hankel1(4, 0.3j) | ||
| def test_spherical_hankel2(self): | ||
| assert sc.spherical_hankel2(4, 0.3j) == cs.spherical_hankel2(4, 0.3j) | ||
| def test_spherical_hankel1_d(self): | ||
| assert sc.spherical_hankel1_d(4, 0.3j) == cs.spherical_hankel1_d(4, 0.3j) | ||
| def test_spherical_hankel2_d(self): | ||
| assert sc.spherical_hankel2_d(4, 0.3j) == cs.spherical_hankel2_d(4, 0.3j) | ||
| def test_yv_d(self): | ||
| assert sc.yv_d(0.4, 0.3j) == cs.yv_d(0.4, 0.3j) | ||
| def test_incgamma(self): | ||
| assert sc.incgamma(1.5, 0.3j) == cs.incgamma(1.5, 0.3j) | ||
| def test_intkambe(self): | ||
| assert sc.intkambe(4, 0.3j, 0.2 + 1j) == cs.intkambe(4, 0.3j, 0.2 + 1j) | ||
| def test_lpmv(self): | ||
| assert sc.lpmv(3, 4, 0.2 + 1j) == cs.lpmv(3, 4, 0.2 + 1j) | ||
| def test_pi_fun(self): | ||
| assert sc.pi_fun(4, 3, 0.2 + 1j) == cs.pi_fun(4, 3, 0.2 + 1j) | ||
| def test_sph_harm(self): | ||
| assert sc.sph_harm(3, 4, 1, 0.2 + 1j) == cs.sph_harm(3, 4, 1, 0.2 + 1j) | ||
| def test_tau_fun(self): | ||
| assert sc.tau_fun(4, 3, 0.2 + 1j) == cs.tau_fun(4, 3, 0.2 + 1j) | ||
| def test_tl_vcw(self): | ||
| assert sc.tl_vcw(6, 5, 4, 3, 2, 1, 0) == cs.tl_vcw(6, 5, 4, 3, 2, 1, 0) | ||
| def test_tl_vcw_r(self): | ||
| assert sc.tl_vcw_r(6, 5, 4, 3, 2.0, 1, 0) == cs.tl_vcw_r(6, 5, 4, 3, 2.0, 1, 0) | ||
| def test_tl_vsw_A(self): | ||
| assert sc.tl_vsw_A(6, 5, 4, 3, 2, 1.0, 0) == cs.tl_vsw_A(6, 5, 4, 3, 2, 1.0, 0) | ||
| def test_tl_vsw_rA(self): | ||
| assert sc.tl_vsw_rA(6, 5, 4, 3, 2, 1.0, 0) == cs.tl_vsw_rA( | ||
| 6, 5, 4, 3, 2.0, 1.0, 0 | ||
| ) | ||
| def test_tl_vsw_B(self): | ||
| assert sc.tl_vsw_B(6, 5, 4, 3, 2, 1.0, 0) == cs.tl_vsw_B(6, 5, 4, 3, 2, 1.0, 0) | ||
| def test_tl_vsw_rB(self): | ||
| assert sc.tl_vsw_rB(6, 5, 4, 3, 2, 1.0, 0) == cs.tl_vsw_rB( | ||
| 6, 5, 4, 3, 2.0, 1.0, 0 | ||
| ) | ||
| def test_wigner3j(self): | ||
| assert sc.wigner3j(5, 4, 3, -3, 2, 1) == cs.wigner3j(5, 4, 3, -3, 2, 1) | ||
| def test_wigner3j(self): | ||
| assert sc.wigner3j(5, 4, 3, -3, 2, 1) == cs.wigner3j(5, 4, 3, -3, 2, 1) | ||
| def test_wignersmalld(self): | ||
| assert sc.wignersmalld(5, 4, 3, 2) == cs.wignersmalld(5, 4, 3, 2.0) | ||
| def test_wignerd(self): | ||
| assert sc.wignerd(5, 4, 3, 2, 1, 0) == cs.wignerd(5, 4, 3, 2, 1.0, 0) |
| from treams import sw | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestPeriodicToCw: | ||
| def test_h(self): | ||
| assert isclose( | ||
| sw.periodic_to_cw(1, 2, 0, 3, 2, 0, 3, 2), | ||
| -1.1890388773999045e-17 + 0.19418478507072567j, | ||
| ) | ||
| def test_h_zero(self): | ||
| assert sw.periodic_to_cw(1, 2, 0, 3, -2, 0, 3, 2) == 0 | ||
| def test_p_same(self): | ||
| assert isclose( | ||
| sw.periodic_to_cw(1, 1, 0, 3, 1, 0, 3, 2, poltype="parity"), | ||
| -0.15197363286501506, | ||
| ) | ||
| def test_p_opposite(self): | ||
| assert isclose( | ||
| sw.periodic_to_cw(1, 1, 1, 3, 1, 0, 3, 2, poltype="parity"), | ||
| -0.02171051898071644, | ||
| ) | ||
| def test_p_zero(self): | ||
| assert sw.periodic_to_cw(1, 2, 0, 3, -2, 0, 3, 2, poltype="parity") == 0 | ||
| class TestPeriodicToPw: | ||
| def test_h(self): | ||
| assert isclose( | ||
| sw.periodic_to_pw(1, 2, 3 + 4j, 0, 5, -4, 0, 2), | ||
| -0.021612980340836 + 0.015904360513006j, | ||
| ) | ||
| def test_h_zero(self): | ||
| assert sw.periodic_to_pw(1, 2, 3 + 4j, 0, 5, -4, 1, 2) == 0 | ||
| def test_h_kz_zero(self): | ||
| x = sw.periodic_to_pw(1, 2, 0, 0, 5, -4, 0, 2) | ||
| assert abs(x) > 1e16 and isclose(x.real / x.imag, -1.823529411764707) | ||
| def test_h_kz_neg(self): | ||
| assert isclose( | ||
| sw.periodic_to_pw(1, 2, -3, 0, 5, -4, 0, 2), | ||
| -0.015256789234245 - 0.004449896859988j, | ||
| ) | ||
| def test_p_same(self): | ||
| assert isclose( | ||
| sw.periodic_to_pw(1, 2, 3, 1, 5, -4, 1, 2, poltype="parity"), | ||
| 0.034026205422735 + 0.009924309914964j, | ||
| ) | ||
| def test_p_opposite(self): | ||
| assert isclose( | ||
| sw.periodic_to_pw(1, 2, 3, 1, 5, -4, 0, 2, poltype="parity"), | ||
| -0.049282994656980 - 0.014374206774952j, | ||
| ) | ||
| def test_p_kz_zero(self): | ||
| x = sw.periodic_to_pw(1, 2, 0, 1, 5, -4, 1, 2, poltype="parity") | ||
| assert abs(x) > 1e16 and isclose(x.real / x.imag, -1.823529411764707) | ||
| def test_p_kz_neg(self): | ||
| assert isclose( | ||
| sw.periodic_to_pw(1, 2, -3j, 1, 5, -4, 1, 2, poltype="parity"), | ||
| -0.562764396508645 + 1.929477930886781j, | ||
| ) | ||
| class TestRotate: | ||
| def test(self): | ||
| assert isclose( | ||
| sw.rotate(6, 5, 0, 6, 4, 0, 3, 2, 1), 0.049742012840172 - 0.007540365393637j | ||
| ) | ||
| def test_zero(self): | ||
| assert sw.rotate(8, 7, 1, 6, 5, 0, 4, 3, 2) == 0 | ||
| class TestTranslate: | ||
| def test_sh_real(self): | ||
| assert isclose( | ||
| sw.translate(5, 4, 1, 3, 2, 1, 8, 7, 6), | ||
| 0.085624539859945 - 0.149999063169118j, | ||
| ) | ||
| def test_rh_real(self): | ||
| assert isclose( | ||
| sw.translate(5, 4, 0, 3, 2, 0, 8, 7, 6, singular=False), | ||
| -0.117145153538869 + 0.042172438021060j, | ||
| ) | ||
| def test_sh_zero(self): | ||
| assert sw.translate(5, 4, 1, 3, 2, 0, 8, 7, 6) == 0 | ||
| def test_rh_zero(self): | ||
| assert sw.translate(5, 4, 1, 3, 2, 0, 8, 7, 6, singular=False) == 0 | ||
| def test_sh_kr_zero(self): | ||
| assert isclose(sw.translate(5, 4, 1, 3, 2, 1, 1e-30j, 7, 6), 0) | ||
| def test_sp_real_a(self): | ||
| assert isclose( | ||
| sw.translate(5, 4, 1, 3, 2, 1, 8, 7, 6, "parity"), | ||
| -0.024574996806028 - 0.103410185698746j, | ||
| ) | ||
| def test_sp_real_b(self): | ||
| assert isclose( | ||
| sw.translate(5, 4, 1, 3, 2, 0, 8, 7, 6, "parity"), | ||
| 0.110199536665973 - 0.046588877470371j, | ||
| ) | ||
| def test_rp_real_a(self): | ||
| assert isclose( | ||
| sw.translate(5, 4, 0, 3, 2, 0, 8, 7, 6, "parity", False), | ||
| -0.064322610568195 - 0.040900170567219j, | ||
| ) | ||
| def test_rp_real_b(self): | ||
| assert isclose( | ||
| sw.translate(5, 4, 1, 3, 2, 0, 8, 7, 6, "parity", False), | ||
| 0.052822542970675 - 0.083072608588279j, | ||
| ) | ||
| def test_sp_kr_zero(self): | ||
| assert isclose(sw.translate(5, 4, 1, 3, 2, 1, 1e-30j, 7, 6, "parity"), 0) | ||
| class TestTranslatePeriodic: | ||
| def test_0(self): | ||
| assert ( | ||
| sw.translate_periodic( | ||
| 1, 0, 1, [0, 0, 0], ([1], [0], [1]), in_=([1], [0], [0]) | ||
| ) | ||
| == 0 | ||
| ) | ||
| def test_1(self): | ||
| assert isclose( | ||
| sw.translate_periodic( | ||
| [1, 2], [0, 0], [[1, 0], [0, 1]], [0, 0, 0], ([2], [-1], [0]) | ||
| )[0, 0], | ||
| 14.70796326794897 - 258.1708025505043j, | ||
| ) | ||
| def test_2(self): | ||
| assert isclose( | ||
| sw.translate_periodic( | ||
| 1, | ||
| [0, 0, 0], | ||
| [[1, 0, 0], [0, 1, 0], [0, 0, 1]], | ||
| [0, 0, 0], | ||
| ([3], [3], [0]), | ||
| poltype="parity", | ||
| ), | ||
| -1 + 668.66039240j, | ||
| ) | ||
| def test_3(self): | ||
| assert ( | ||
| sw.translate_periodic( | ||
| [1, 1], | ||
| [0, 0], | ||
| [[1, 0], [0, 1]], | ||
| [0, 0, 0], | ||
| ([1], [0], [0]), | ||
| in_=([2], [0], [1]), | ||
| poltype="parity", | ||
| ) | ||
| == 0 | ||
| ) |
| import copy | ||
| import numpy as np | ||
| import treams | ||
| from treams import TMatrix | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestInit: | ||
| def test_simple(self): | ||
| tm = TMatrix(np.eye(6), k0=1) | ||
| assert ( | ||
| np.all(tm == np.eye(6)) | ||
| and tm.k0 == 1 | ||
| and tm.material == (1, 1, 0) | ||
| and tm.basis == treams.SphericalWaveBasis.default(1) | ||
| and tm.poltype == treams.config.POLTYPE | ||
| ) | ||
| def test_complex(self): | ||
| tm = TMatrix( | ||
| np.diag([1, 2]), | ||
| k0=3, | ||
| material=[2, 8, 1], | ||
| basis=treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]], [1, 0, 0]), | ||
| ) | ||
| assert ( | ||
| np.all(tm == np.diag([1, 2])) | ||
| and tm.k0 == 3 | ||
| and tm.material == (2, 8, 1) | ||
| and tm.basis == treams.SphericalWaveBasis([[1, 0, 0], [1, 0, 1]], [1, 0, 0]) | ||
| ) | ||
| class TestSphere: | ||
| def test(self): | ||
| tm = TMatrix.sphere(2, 3, 4, [(2, 1, 1), (9, 1, 2)]) | ||
| del tm.modetype | ||
| m = treams.coeffs.mie([1, 2], [12], [2, 9], [1, 1], [1, 2]) | ||
| assert ( | ||
| np.all(np.diag(tm)[:6:2] == m[0, 1, 1]) | ||
| and np.all(np.diag(tm)[1:6:2] == m[0, 0, 0]) | ||
| and np.all(np.diag(tm)[6::2] == m[1, 1, 1]) | ||
| and np.all(np.diag(tm)[7::2] == m[1, 0, 0]) | ||
| and np.all(np.diag(tm, -1)[:6:2] == m[0, 0, 1]) | ||
| and np.all(np.diag(tm, 1)[:6:2] == m[0, 1, 0]) | ||
| and np.all(np.diag(tm, -1)[6::2] == m[1, 0, 1]) | ||
| and np.all(np.diag(tm, 1)[6::2] == m[1, 1, 0]) | ||
| and np.all(np.diag(tm, -1)[1:6:2] == 0) | ||
| and np.all(np.diag(tm, 1)[1:6:2] == 0) | ||
| and np.all(np.diag(tm, -1)[7::2] == 0) | ||
| and np.all(np.diag(tm, 1)[7::2] == 0) | ||
| and np.all(np.triu(tm, 2) == 0) | ||
| and np.all(np.tril(tm, -2) == 0) | ||
| and tm.k0 == 3 | ||
| and tm.material == (9, 1, 2) | ||
| and tm.poltype == "helicity" | ||
| and tm.basis == treams.SphericalWaveBasis.default(2) | ||
| ) | ||
| class TestProperties: | ||
| def test_xs_ext_avg(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.xs_ext_avg, 2.9294930236877077) | ||
| def test_xs_ext_avg_kappa_zero(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 3), (9, 4)]) | ||
| assert isclose(tm.xs_ext_avg, 0.15523595021864234) | ||
| def test_xs_sca_avg(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.xs_sca_avg, 1.6603264386283758) | ||
| def test_xs_sca_avg_kappa_zero(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 3), (9, 4)]) | ||
| assert isclose(tm.xs_sca_avg, 0.08434021223849283) | ||
| def test_cd(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.cd, -0.9230263013362784) | ||
| def test_chi(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.chi, 0.7483463517622965) | ||
| def test_db(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.db, 0.6085814346536764) | ||
| def test_modes(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| l, m, pol = tm.basis.lms | ||
| assert ( | ||
| np.all(l == 6 * [1] + 10 * [2]) | ||
| and np.all(m == [-1, -1, 0, 0, 1, 1, -2, -2, -1, -1, 0, 0, 1, 1, 2, 2]) | ||
| and np.all(pol == [int((i + 1) % 2) for i in range(16)]) | ||
| ) | ||
| class TestXs: | ||
| def test(self): | ||
| tm = TMatrix.sphere(2, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| illu = treams.plane_wave([0, 0, 1], [0.5, 0], k0=tm.k0, material=tm.material) | ||
| xs = tm.xs(illu, 0.125) | ||
| assert isclose(xs[0], 3.194830855171616,) and isclose(xs[1], 5.63547158) | ||
| class TestTranslate: | ||
| def test(self): | ||
| tm = TMatrix.sphere(3, 0.1, [0.2], [(2 + 1j, 1.1, 1), (9, 1, 2)]) | ||
| m = copy.deepcopy(tm) | ||
| rs = np.array([[0.1, 0.2, 0.3], [-0.4, -0.5, -0.4]]) | ||
| tm = tm.translate(rs[0]) | ||
| tm = tm.translate(rs[1]) | ||
| tm = tm.translate(-rs[0] - rs[1]) | ||
| assert np.all(np.abs(tm - m) < 1e-8) | ||
| def test_kappa_zero(self): | ||
| tm = TMatrix.sphere(3, 0.1, [0.2], [(2 + 1j, 1.1), (9, 1)]) | ||
| m = copy.deepcopy(tm) | ||
| rs = np.array([[0.1, 0.2, 0.3], [-0.4, -0.5, -0.4]]) | ||
| tm.translate(rs[0]) | ||
| tm.translate(rs[1]) | ||
| tm.translate(-rs[0] - rs[1]) | ||
| assert np.all(np.abs(tm - m) < 1e-8) | ||
| class TestClusterRotate: | ||
| def test(self): | ||
| tms = [TMatrix.sphere(3, 0.1, [0.1], [i * i, 1]) for i in range(1, 5)] | ||
| rs1 = np.array([[0, 0, 0], [0.2, 0, 0], [0, 0.2, 0], [0, 0, 0.2]]) | ||
| tm1 = TMatrix.cluster(tms, rs1) | ||
| tm1 = tm1.interaction.solve().expand(treams.SphericalWaveBasis.default(3)) | ||
| tm1 = tm1.rotate(1, 2, 3) | ||
| a = np.array([[np.cos(1), -np.sin(1), 0], [np.sin(1), np.cos(1), 0], [0, 0, 1]]) | ||
| b = np.array([[np.cos(2), 0, np.sin(2)], [0, 1, 0], [-np.sin(2), 0, np.cos(2)]]) | ||
| c = np.array([[np.cos(3), -np.sin(3), 0], [np.sin(3), np.cos(3), 0], [0, 0, 1]]) | ||
| rs2 = (a @ b @ c @ rs1.T).T | ||
| tm2 = TMatrix.cluster(tms, rs2) | ||
| tm2 = tm2.interaction.solve().expand(treams.SphericalWaveBasis.default(3)) | ||
| assert np.all(np.abs(tm1 - tm2) < 1e-16) |
| import numpy as np | ||
| import treams | ||
| from treams import TMatrixC | ||
| def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): | ||
| return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) | ||
| class TestInit: | ||
| def test_simple(self): | ||
| tm = TMatrixC(np.eye(6), k0=1) | ||
| assert ( | ||
| np.all(tm == np.eye(6)) | ||
| and tm.k0 == 1 | ||
| and tm.material.epsilon == 1 | ||
| and tm.material.mu == 1 | ||
| and tm.material.kappa == 0 | ||
| and np.all(tm.basis.positions == [[0, 0, 0]]) | ||
| and tm.poltype == "helicity" | ||
| and np.all(tm.basis.kz == 6 * [0]) | ||
| and np.all(tm.basis.m == [-1, -1, 0, 0, 1, 1]) | ||
| and np.all(tm.basis.pol == [1, 0, 1, 0, 1, 0]) | ||
| and np.all(tm.basis.pidx == [0, 0, 0, 0, 0, 0]) | ||
| and np.all(tm.ks == [1, 1]) | ||
| ) | ||
| def test_complex(self): | ||
| tm = TMatrixC( | ||
| np.diag([1, 2]), | ||
| k0=3, | ||
| material=treams.Material(2, 8, 1), | ||
| basis=treams.CylindricalWaveBasis([[1, 0, 0], [1, 0, 1]], [1, 0, 0]), | ||
| ) | ||
| assert ( | ||
| np.all(tm == np.diag([1, 2])) | ||
| and tm.k0 == 3 | ||
| and tm.material.epsilon == 2 | ||
| and tm.material.mu == 8 | ||
| and tm.material.kappa == 1 | ||
| and np.all(tm.basis.positions == [[1, 0, 0]]) | ||
| and tm.poltype == "helicity" | ||
| and np.all(tm.basis.kz == [1, 1]) | ||
| and np.all(tm.basis.m == [0, 0]) | ||
| and np.all(tm.basis.pol == [0, 1]) | ||
| and np.all(tm.basis.pidx == [0, 0]) | ||
| and np.all(tm.ks == [9, 15]) | ||
| ) | ||
| class TestCylinder: | ||
| def test(self): | ||
| tm = TMatrixC.cylinder([1], 2, 3, 4, [(2, 1, 1), (9, 1, 2)]) | ||
| m = treams.coeffs.mie_cyl(1, [-2, -1, 0, 1, 2], 3, [4], [2, 9], [1, 1], [1, 2]) | ||
| assert ( | ||
| tm[0, 0] == m[0, 1, 1] | ||
| and tm[1, 1] == m[0, 0, 0] | ||
| and tm[0, 1] == m[0, 1, 0] | ||
| and tm[1, 0] == m[0, 0, 1] | ||
| and tm[2, 2] == m[1, 1, 1] | ||
| and tm[3, 3] == m[1, 0, 0] | ||
| and tm[2, 3] == m[1, 1, 0] | ||
| and tm[3, 2] == m[1, 0, 1] | ||
| and tm[4, 4] == m[2, 1, 1] | ||
| and tm[5, 5] == m[2, 0, 0] | ||
| and tm[4, 5] == m[2, 1, 0] | ||
| and tm[5, 4] == m[2, 0, 1] | ||
| and tm[6, 6] == m[3, 1, 1] | ||
| and tm[7, 7] == m[3, 0, 0] | ||
| and tm[6, 7] == m[3, 1, 0] | ||
| and tm[7, 6] == m[3, 0, 1] | ||
| and tm[8, 8] == m[4, 1, 1] | ||
| and tm[9, 9] == m[4, 0, 0] | ||
| and tm[8, 9] == m[4, 1, 0] | ||
| and tm[9, 8] == m[4, 0, 1] | ||
| and tm.k0 == 3 | ||
| and tm.material.epsilon == 9 | ||
| and tm.material.mu == 1 | ||
| and tm.material.kappa == 2 | ||
| and np.all(tm.basis.positions == [[0, 0, 0]]) | ||
| and tm.poltype == "helicity" | ||
| and np.all(tm.basis.kz == 10 * [1]) | ||
| and np.all(tm.basis.m == [-2, -2, -1, -1, 0, 0, 1, 1, 2, 2]) | ||
| and np.all(tm.basis.pol == [int((i + 1) % 2) for i in range(10)]) | ||
| and np.all(tm.basis.pidx == 10 * [0]) | ||
| and np.all(tm.ks == [3, 15]) | ||
| ) | ||
| class TestProperties: | ||
| def test_xw_ext_avg(self): | ||
| tm = TMatrixC.cylinder([-1, 1], 1, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.xw_ext_avg, 1.2047234457348013) | ||
| def test_xw_ext_avg_kappa_zero(self): | ||
| tm = TMatrixC.cylinder([-1, 1], 1, 3, [4], [(2 + 1j, 3), (9,)]) | ||
| assert isclose(tm.xw_ext_avg, 0.6661748147466017) | ||
| def test_xw_sca_avg(self): | ||
| tm = TMatrixC.cylinder([-1, 1], 1, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| assert isclose(tm.xw_sca_avg, 0.6847986332271352) | ||
| def test_xw_sca_avg_kappa_zero(self): | ||
| tm = TMatrixC.cylinder([-1, 1], 1, 3, [4], [(2 + 1j, 3), (9, 4)]) | ||
| assert isclose(tm.xw_sca_avg, 0.18067830829683562) | ||
| def test_krho(self): | ||
| tm = TMatrixC.cylinder([0, 5], 1, 3, [1], [(2 + 1j,), ()]) | ||
| assert np.all( | ||
| np.abs(tm.krhos - [3, 3, 3, 3, 3, 3, 4j, 4j, 4j, 4j, 4j, 4j]) < 1e-16 | ||
| ) | ||
| def test_modes(self): | ||
| tm = TMatrixC.cylinder([-1, 1], 1, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| kz, m, pol = tm.basis.zms | ||
| assert ( | ||
| np.all(kz == 6 * [-1] + 6 * [1]) | ||
| and np.all(m == [-1, -1, 0, 0, 1, 1, -1, -1, 0, 0, 1, 1]) | ||
| and np.all(pol == [int((i + 1) % 2) for i in range(12)]) | ||
| ) | ||
| def test_fullmodes(self): | ||
| tm = TMatrixC.cylinder([-1, 1], 1, 3, [4], [(2 + 1j, 1, 1), (9, 1, 2)]) | ||
| pidx, kz, m, pol = tm.basis[()] | ||
| assert ( | ||
| np.all(kz == 6 * [-1] + 6 * [1]) | ||
| and np.all(m == [-1, -1, 0, 0, 1, 1, -1, -1, 0, 0, 1, 1]) | ||
| and np.all(pol == [int((i + 1) % 2) for i in range(12)]) | ||
| and np.all(pidx == 12 * [0]) | ||
| ) |
| import copy | ||
| import numpy as np | ||
| import pytest | ||
| from treams import util | ||
| class TestAnnotationDict: | ||
| def test_init_dict(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| assert a == {"a": 1, "b": 2} | ||
| def test_init_tuple(self): | ||
| a = util.AnnotationDict((("a", 1), ("b", 2))) | ||
| assert a == {"a": 1, "b": 2} | ||
| def test_init_anndict(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| b = util.AnnotationDict(a) | ||
| assert a == b | ||
| def test_kwargs(self): | ||
| with pytest.warns(util.AnnotationWarning): | ||
| a = util.AnnotationDict({"a": 0, "c": 3}, a=1, b=2) | ||
| assert a == {"a": 1, "b": 2, "c": 3} | ||
| def test_getitem(self): | ||
| a = util.AnnotationDict({"a": 1}) | ||
| assert a["a"] == 1 | ||
| def test_delitem(self): | ||
| a = util.AnnotationDict({"a": 1}) | ||
| del a["a"] | ||
| assert a == {} | ||
| def test_iter(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| assert ["a", "b"] == list(a) | ||
| def test_len(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| assert len(a) == 2 | ||
| def test_repr(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| assert repr(a) == "AnnotationDict({'a': 1, 'b': 2})" | ||
| def test_match(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| b = {"a": 1, "c": 3} | ||
| assert a.match(b) is None | ||
| def test_match_fail(self): | ||
| a = util.AnnotationDict({"a": 1, "b": 2}) | ||
| b = {"a": 2, "c": 3} | ||
| with pytest.warns(util.AnnotationWarning): | ||
| a.match(b) | ||
| class TestAnnotationSequence: | ||
| def test_init(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}) | ||
| assert a == ({"a": 1}, {"a": 2}) | ||
| def test_repr(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}, mapping=dict) | ||
| assert repr(a) == "AnnotationSequence({'a': 1}, {'a': 2})" | ||
| def test_len(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}) | ||
| assert len(a) == 2 | ||
| def test_getitem_int(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}) | ||
| assert a[1] == {"a": 2} | ||
| def test_getitem_tuple(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2})[()] | ||
| assert a == ({"a": 1}, {"a": 2}) and isinstance(a, tuple) | ||
| def test_getitem_slice(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}) | ||
| assert a[1:] == ({"a": 2},) and isinstance(a, util.AnnotationSequence) | ||
| def test_getitem_list(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}) | ||
| assert a[[1, 0, 1]] == ({"a": 2}, {"a": 1}, {"a": 2}) | ||
| def test_getitem_error(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"a": 2}) | ||
| with pytest.raises(TypeError): | ||
| a["fail"] | ||
| def test_update_warn_len(self): | ||
| a = util.AnnotationSequence({}) | ||
| with pytest.warns(util.AnnotationWarning): | ||
| a.update(({}, {})) | ||
| def test_update_warn_overwrite(self): | ||
| a = util.AnnotationSequence({"a": 1}) | ||
| with pytest.warns(util.AnnotationWarning): | ||
| a.update(({"a": 2},)) | ||
| def test_update(self): | ||
| a = util.AnnotationSequence({"a": 1}, {"b": 2}) | ||
| a.update(({"c": 3}, {"a": 4})) | ||
| assert a == util.AnnotationSequence({"a": 1, "c": 3}, {"b": 2, "a": 4}) | ||
| def test_match_warn_overwrite(self): | ||
| a = util.AnnotationSequence({"a": 1}) | ||
| with pytest.warns(util.AnnotationWarning): | ||
| a.match(({"a": 2},)) | ||
| def test_match(self): | ||
| a = util.AnnotationSequence({"a": 1}) | ||
| assert a.match(({"b": 2},)) is None | ||
| def test_eq_error_len_get(self): | ||
| assert not (util.AnnotationSequence({}) == 1) | ||
| def test_eq_error_len(self): | ||
| assert not (util.AnnotationSequence({}, {}) == (None,)) | ||
| def test_eq_false(self): | ||
| assert not ( | ||
| util.AnnotationSequence({"a": 1}) == util.AnnotationSequence({"a": 2}) | ||
| ) | ||
| def test_eq_true(self): | ||
| assert util.AnnotationSequence({"a": 1}) == util.AnnotationSequence({"a": 1}) | ||
| def test_add(self): | ||
| assert util.AnnotationSequence({"a": 1}) + ( | ||
| {"b": 2}, | ||
| ) == util.AnnotationSequence({"a": 1}, {"b": 2}) | ||
| def test_radd(self): | ||
| assert ({"a": 1},) + util.AnnotationSequence( | ||
| {"b": 2} | ||
| ) == util.AnnotationSequence({"a": 1}, {"b": 2}) | ||
| class TestSequenceAsDict: | ||
| def test_set(self): | ||
| ann = util.AnnotationSequence({}, {}) | ||
| ann.as_dict = {"a": (1, 2)} | ||
| assert ann.as_dict["a"] == (1, 2) | ||
| def test_getitem(self): | ||
| ann = util.AnnotationSequence({"a": 1}, {"a": 1}) | ||
| assert ann.as_dict["a"] == (1, 1) | ||
| def test_getitem_missing(self): | ||
| ann = util.AnnotationSequence({}) | ||
| with pytest.raises(KeyError): | ||
| ann.as_dict["fail"] | ||
| def test_setitem_delnone(self): | ||
| ann = util.AnnotationSequence({"a": 1}, {}) | ||
| ann.as_dict["a"] = (1,) | ||
| assert ann.as_dict["a"] == (None, 1) | ||
| def test_delitem(self): | ||
| ann = util.AnnotationSequence({"a": 1}) | ||
| del ann.as_dict["a"] | ||
| assert ann == util.AnnotationSequence({}) | ||
| def test_delitem_error(self): | ||
| ann = util.AnnotationSequence({}) | ||
| with pytest.raises(KeyError): | ||
| del ann.as_dict["fail"] | ||
| def test_iter(self): | ||
| ann = util.AnnotationSequence({"a": 1}, {"a": 1, "b": 2}) | ||
| assert set(iter(ann.as_dict)) == {"a", "b"} | ||
| def test_len(self): | ||
| ann = util.AnnotationSequence({"a": 1, "b": 2}) | ||
| assert len(ann.as_dict) == 2 | ||
| def test_repr(self): | ||
| ann = util.AnnotationSequence({"a": 1}, {"a": 1, "b": 2}) | ||
| assert repr(ann.as_dict) in ( | ||
| "SequenceAsDict({'a': (1, 1), 'b': (None, 2)})", | ||
| "SequenceAsDict({'b': (None, 2), 'a': (1, 1)})", | ||
| ) | ||
| class TestAnnotatedArray: | ||
| def test_init(self): | ||
| arr = util.AnnotatedArray([1, 2, 3], a=(1,)) | ||
| assert (arr == [1, 2, 3]).all() and arr.ann == ({"a": 1},) | ||
| def test_getattr_same(self): | ||
| arr = util.AnnotatedArray([[0, 1], [2, 3], [4, 5]], a=(0, 0)) | ||
| assert arr.a == 0 | ||
| def test_getattr(self): | ||
| arr = util.AnnotatedArray([[0, 1], [2, 3], [4, 5]], a=(0, 1)) | ||
| assert arr.a == (0, 1) | ||
| def test_setattr(self): | ||
| arr = util.AnnotatedArray([[0, 1], [2, 3], [4, 5]], a=(1,)) | ||
| arr.a = (0, 1) | ||
| assert arr.a == (0, 1) | ||
| def test_delattr(self): | ||
| arr = util.AnnotatedArray([[0, 1], [2, 3], [4, 5]], a=(1,)) | ||
| del arr.a | ||
| assert arr.ann == util.AnnotationSequence({}, {}) | ||
| def test_copy(self): | ||
| a = util.AnnotatedArray([[0, 1], [2, 3], [4, 5]], a=(1,)) | ||
| b = copy.copy(a) | ||
| assert (a == b).all() and a.ann == b.ann and (a is not b) | ||
| def test_ufunc_out(self): | ||
| a = util.AnnotatedArray([[2, 3], [4, 5]], a=(2, 1)) | ||
| b = util.AnnotatedArray([2, 4], a=(1,)) | ||
| x = util.AnnotatedArray([[-1, -1], [-1, -1]]) | ||
| y = util.AnnotatedArray([[-1, -1], [-1, -1]]) | ||
| np.divmod(a, b, out=(x, y)) | ||
| assert ( | ||
| (x == [[1, 0], [2, 1]]).all() | ||
| and (y == [[0, 3], [0, 1]]).all() | ||
| and x.ann == y.ann == a.ann | ||
| ) | ||
| def test_bool(self): | ||
| a = util.AnnotatedArray([0, 0], a=(0,)) | ||
| with pytest.raises(ValueError): | ||
| bool(a) | ||
| def test_bool(self): | ||
| a = util.AnnotatedArray([1], a=(0,)) | ||
| assert bool(a) | ||
| def test_ufunc_reduce(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| y = np.add.reduce(x) | ||
| assert (y == [4, 6]).all() and y.ann == ({"a": 1},) | ||
| def test_ufunc_at(self): | ||
| arr = util.AnnotatedArray([[2, 3], [4, 5]], a=(2, 1)) | ||
| np.add.at(arr, 1, [1, 2]) | ||
| assert (arr == [[2, 3], [5, 7]]).all() and arr.a == (2, 1) | ||
| def test_ufunc_outer(self): | ||
| x = util.AnnotatedArray([[2, 3], [4, 5]], a=(2, 1)) | ||
| y = util.AnnotatedArray([10, -1], a=(3,), b=(4,)) | ||
| z = np.add.outer(x, y) | ||
| assert ( | ||
| (z == [[[12, 1], [13, 2]], [[14, 3], [15, 4]]]).all() | ||
| and z.a == (2, 1, 3) | ||
| and z.b == (None, None, 4) | ||
| ) | ||
| def test_gufunc(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| y = util.AnnotatedArray([[[1], [-1]], [[10], [-1]]], a=(3, 1, None), b=(4, 4)) | ||
| z = x @ y | ||
| assert (z == [[[-1], [-1]], [[8], [26]]]).all() and z.ann == ( | ||
| {"a": 3}, | ||
| {"a": 2}, | ||
| {"b": 4}, | ||
| ) | ||
| def test_str(self): | ||
| assert str(util.AnnotatedArray([0, 1], a=(1,))) == "[0 1]" | ||
| def test_repr(self): | ||
| assert ( | ||
| repr(util.AnnotatedArray([0, 1], a=(1,))) | ||
| == """AnnotatedArray( | ||
| [0, 1], | ||
| AnnotationSequence(AnnotationDict({'a': 1})), | ||
| )""" | ||
| ) | ||
| def test_getitem_slice(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| y = x[:, 0] | ||
| assert (y == [1, 3]).all() and y.ann == ({"a": 2},) | ||
| def test_getitem_none(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| y = x[:, None, 0] | ||
| assert (y == [[1], [3]]).all() and y.ann == ({"a": 2}, {}) | ||
| def test_getitem_ellipsis(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| y = x[1, ...] | ||
| assert (y == [3, 4]).all() and y.ann == ({"a": 1},) | ||
| def test_getitem_fancy(self): | ||
| x = util.AnnotatedArray([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], a=(2, 1, 2)) | ||
| y = x[[True, False], :, [1, 0]] | ||
| assert (y == [[1, 3], [0, 2]]).all() and y.ann == ({"a": 2}, {"a": 1}) | ||
| def test_getitem_ellipsis_implicit(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| y = x[1] | ||
| assert (y == [3, 4]).all() and y.ann == ({"a": 1},) | ||
| def test_setitem_slice(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(0, 1)) | ||
| x[:, 1] = util.AnnotatedArray([5, 6], a=(0,)) | ||
| assert (x == [[1, 5], [3, 6]]).all() and x.ann == ({"a": 0}, {"a": 1}) | ||
| def test_setitem_none(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| x[:, None, 1] = util.AnnotatedArray([[5], [6]], a=(2, -1)) | ||
| assert (x == [[1, 5], [3, 6]]).all() and x.ann == ({"a": 2}, {"a": 1}) | ||
| def test_setitem_ellipsis(self): | ||
| x = util.AnnotatedArray([[1, 2], [3, 4]], a=(2, 1)) | ||
| x[..., 1] = util.AnnotatedArray([5, 6], a=(2,)) | ||
| assert (x == [[1, 5], [3, 6]]).all() and x.ann == ({"a": 2}, {"a": 1}) | ||
| def test_setitem_fancy(self): | ||
| x = util.AnnotatedArray([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], a=(1, 2, 1)) | ||
| x[[True, False], :, [1, 0]] = util.AnnotatedArray( | ||
| [[-1, -2], [-3, -4]], a=(1, 2), b=(-1, -2) | ||
| ) | ||
| assert (x == [[[-3, -1], [-4, -2]], [[4, 5], [6, 7]]]).all() and x.ann == ( | ||
| {"a": 1}, | ||
| {"a": 2}, | ||
| {"a": 1}, | ||
| ) | ||
| def test_transpose(self): | ||
| x = util.AnnotatedArray([[0, 1], [2, 3]], a=(2, 3)).T | ||
| assert (x == [[0, 2], [1, 3]]).all() and x.a == (3, 2) | ||
| def test_any_empty(self): | ||
| assert not util.AnnotatedArray([]).any() | ||
| def test_any(self): | ||
| x = util.AnnotatedArray([[1, 1], [1, 0], [0, 0]], a=(1, 2)).any( | ||
| 1, keepdims=True | ||
| ) | ||
| assert (x == [[True], [True], [False]]).all() and x.a == (1, 2) | ||
| def test_all_empty(self): | ||
| assert util.AnnotatedArray([]).all() | ||
| def test_all(self): | ||
| x = util.AnnotatedArray([[1, 1], [1, 0], [0, 0]], a=(1, 2)).all(1) | ||
| assert (x == [True, False, False]).all() and x.ann == ({"a": 1},) | ||
| def test_max(self): | ||
| assert util.AnnotatedArray([0, 1, 2], a=(1,)).max() == 2 | ||
| def test_min(self): | ||
| assert util.AnnotatedArray([0, 1, 2], a=(1,)).min() == 0 | ||
| def test_sum(self): | ||
| assert np.sum(util.AnnotatedArray([1, 2]), initial=-1) == 2 | ||
| def test_prod(self): | ||
| assert np.prod(util.AnnotatedArray([1, 2]), initial=-1) == -2 | ||
| def test_cumsum(self): | ||
| x = util.AnnotatedArray([[1], [0], [2]], a=(1, 1)).cumsum() | ||
| assert (x == [1, 1, 3]).all() and x.ann == ({"a": 1},) | ||
| def test_cumsum_axis(self): | ||
| x = util.AnnotatedArray([[1, 1], [1, 0], [0, 0]], a=(1, 2)).cumsum(1) | ||
| assert (x == [[1, 2], [1, 1], [0, 0]]).all() and x.a == (1, 2) | ||
| def test_cumprod(self): | ||
| x = util.AnnotatedArray([1, -1, 2], a=(1,)).cumprod() | ||
| assert (x == [1, -1, -2]).all() and x.ann == ({"a": 1},) | ||
| def test_cumprod_axis(self): | ||
| x = util.AnnotatedArray([[1, 1], [1, 0], [0, 0]], a=(1, 2)).cumprod(1) | ||
| assert (x == [[1, 1], [1, 0], [0, 0]]).all() and x.a == (1, 2) | ||
| def test_trace(self): | ||
| x = util.AnnotatedArray([[[1, 2, 3], [4, 5, 6]]], a=(1, 2, 3)).trace(1, 1, 2) | ||
| assert x == 8 and x.ann.as_dict["a"] == (1,) | ||
| def test_imag(self): | ||
| x = util.AnnotatedArray([1 + 2j, 3 + 4j], a=(1,)).imag | ||
| assert (x == [2, 4]).all() and x.ann.as_dict["a"] == (1,) | ||
| def test_real(self): | ||
| x = util.AnnotatedArray([1 + 2j, 3 + 4j], a=(1,)).real | ||
| assert (x == [1, 3]).all() and x.ann.as_dict["a"] == (1,) | ||
| def test_conjugate(self): | ||
| x = util.AnnotatedArray([1 + 2j, 3 + 4j], a=(1,)).conjugate() | ||
| assert (x == [1 - 2j, 3 - 4j]).all() and x.ann.as_dict["a"] == (1,) | ||
| def test_diagonal(self): | ||
| x = util.AnnotatedArray([[[1, 2, 3], [4, 5, 6]]], a=(1, 2, 3)).diagonal(1, 0, 2) | ||
| assert (x == [[2], [5]]).all() and x.ann.as_dict["a"] == (2, None) | ||
| class TestImplements: | ||
| def test_solve(self): | ||
| m = util.AnnotatedArray([[1, 2], [3, -1]], a=(3, 1), b=(1,)) | ||
| b = util.AnnotatedArray([7, -7], a=(3,)) | ||
| a = np.linalg.solve(m, b) | ||
| assert np.all(np.abs(a - [-1, 4]) < 1e-14) and a.ann == ({"a": 1, "b": 1},) | ||
| def test_solve_multiple(self): | ||
| m = util.AnnotatedArray([[1, 2], [3, -1]], a=(3, 1), b=(1,)) | ||
| b = util.AnnotatedArray([[7, 3], [-7, 2]], a=(3, 2), b=(3, None)) | ||
| a = np.linalg.solve(m, b) | ||
| assert np.all(np.abs(a - [[-1, 1], [4, 1]]) < 1e-14) and a.ann == ( | ||
| {"a": 1, "b": 1}, | ||
| {"a": 2}, | ||
| ) | ||
| def test_lstsq(self): | ||
| m = util.AnnotatedArray([[1, 2], [3, -1]], a=(3, 1), b=(1,)) | ||
| b = util.AnnotatedArray([7, -7], a=(3,)) | ||
| a = np.linalg.lstsq(m, b, None)[0] | ||
| assert (np.abs(a - [-1, 4]) < 1e-14).all() and a.ann == ({"a": 1, "b": 1},) | ||
| def test_lstsq_multiple(self): | ||
| m = util.AnnotatedArray([[1, 2], [3, -1]], a=(3, 1), b=(1,)) | ||
| b = util.AnnotatedArray([[7, 3], [-7, 2]], a=(3, 2), b=(3, None)) | ||
| a = np.linalg.lstsq(m, b, None)[0] | ||
| assert (np.abs(a - [[-1, 1], [4, 1]]) < 1e-14).all() and a.ann == ( | ||
| {"a": 1, "b": 1}, | ||
| {"a": 2}, | ||
| ) | ||
| def test_svd(self): | ||
| m = util.AnnotatedArray([[5, 2], [2, 2]], a=(3, 1)) | ||
| u, s, v = np.linalg.svd(m) | ||
| assert ( | ||
| (np.abs(s - [6, 1]) < 1e-14).all() and u.a == (3, None) and v.a == (None, 1) | ||
| ) | ||
| def test_svd_no_uv(self): | ||
| m = util.AnnotatedArray([[5, 2], [2, 2]], a=(3, 1)) | ||
| s = np.linalg.svd(m, compute_uv=False) | ||
| assert (np.abs(s - [6, 1]) < 1e-14).all() | ||
| def test_diag(self): | ||
| x = np.diag(util.AnnotatedArray([[0, 1], [2, 3]], a=(3, 3))) | ||
| assert (x == [0, 3]).all() and x.a == 3 | ||
| def test_diag_create(self): | ||
| x = np.diag(util.AnnotatedArray([1], a=(2,)), -1) | ||
| assert (x == [[0, 0], [1, 0]]).all() and x.a == 2 | ||
| def test_tril(self): | ||
| x = np.tril(util.AnnotatedArray([[1, 2], [3, 4]], a=(5, 6))) | ||
| assert (x == [[1, 0], [3, 4]]).all() and x.a == (5, 6) | ||
| def test_triu(self): | ||
| x = np.triu(util.AnnotatedArray([[1, 2], [3, 4]], a=(5, 6)), 1) | ||
| assert (x == [[0, 2], [0, 0]]).all() and x.a == (5, 6) | ||
| def test_zeros_like(self): | ||
| x = np.zeros_like(util.AnnotatedArray([1, 2], a=1)) | ||
| assert (x == [0, 0]).all() and x.dtype == int and isinstance(x, np.ndarray) | ||
| def test_ones_like(self): | ||
| x = np.ones_like(util.AnnotatedArray([1, 2], a=1)) | ||
| assert (x == [1, 1]).all() and x.dtype == int and isinstance(x, np.ndarray) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
0
-100%0
-100%0
-100%