========= 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]], )