Source code for fdtd.detectors

"""This module implement detectors."""
from __future__ import annotations

import logging
from functools import partial
from typing import Optional, Tuple

import matplotlib.pyplot as plt
import numpy as np

from .bases import FDTDElementBase
from .utils import Direction

logger = logging.getLogger(__name__)


class Detector(FDTDElementBase):
    """Base class for all detectors.

    Parameters
    ----------
    x_min : float
        Minimum x coordinate of the bounding box containing the detector.
    y_min : float
        Minimum y coordinate of the bounding box containing the detector.
    z_min : float
        Minimum z coordinate of the bounding box containing the detector.
    x_max : float
        Maximum x coordinate of the bounding box containing the detector.
    y_max : float
        Maximum y coordinate of the bounding box containing the detector.
    z_max : float
        Maximum z coordinate of the bounding box containing the detector.
    name : Optional[str]
        Name of the source.
    plot : bool
        Flag to enable plotting.
    """

    def __init__(
        self,
        x_min: float,
        y_min: float,
        z_min: float,
        x_max: float,
        y_max: float,
        z_max: float,
        name: Optional[str] = None,
        direction: Direction = Direction.Z,
        plot: bool = False,
    ):
        """Initialize the object."""
        super().__init__()
        self.x_min = x_min
        self.y_min = y_min
        self.z_min = z_min
        self.x_max = x_max
        self.y_max = y_max
        self.z_max = z_max
        self.name = name
        self.direction = direction
        self.captured: Optional[np.ndarray] = None
        self._plot = plot

        self.i_s: Optional[slice] = None
        self.j_s: Optional[slice] = None
        self.k_s: Optional[slice] = None
        self.idx_s: Optional[Tuple] = None
        self.idx_e: Optional[Tuple] = None

    def update(self):
        """Update detectors."""

    def attach_to_grid(self):
        """Attach object to grid."""

    def plot_3d(self, ax, alpha=0.5):
        """Plot a source and attach to an axis."""

    def plot(self):
        """Plot."""


[docs]class VoltageDetector(Detector): """Model of a voltage detector. Parameters ---------- x_min : float Minimum x coordinate of the bounding box containing the detector. y_min : float Minimum y coordinate of the bounding box containing the detector. z_min : float Minimum z coordinate of the bounding box containing the detector. x_max : float Maximum x coordinate of the bounding box containing the detector. y_max : float Maximum y coordinate of the bounding box containing the detector. z_max : float Maximum z coordinate of the bounding box containing the detector. name : Optional[str] Name of the source. plot : bool Flag to enable plotting. """ def attach_to_grid(self): """Attach object to grid.""" s = self.idx_s = ( np.argmin(np.abs(self.grid.x - self.x_min)), np.argmin(np.abs(self.grid.y - self.y_min)), np.argmin(np.abs(self.grid.z - self.z_min)), ) e = self.idx_e = ( np.argmin(np.abs(self.grid.x - self.x_max)), np.argmin(np.abs(self.grid.y - self.y_max)), np.argmin(np.abs(self.grid.z - self.z_max)), ) dx, dy, dz = self.grid.spacing if self.direction == Direction.X: self.i_s = slice(self.idx_s[0], self.idx_e[0]) self.j_s = slice(self.idx_s[1], self.idx_e[1] + 1) self.k_s = slice(self.idx_s[2], self.idx_e[2] + 1) self.c_v = -dx / ((e[1] - s[1] + 1) * (e[2] - s[2] + 1)) elif self.direction == Direction.Y: self.i_s = slice(self.idx_s[0], self.idx_e[0] + 1) self.j_s = slice(self.idx_s[1], self.idx_e[1]) self.k_s = slice(self.idx_s[2], self.idx_e[2] + 1) self.c_v = -dy / ((e[2] - s[2] + 1) * (e[0] - s[0] + 1)) else: self.i_s = slice(self.idx_s[0], self.idx_e[0] + 1) self.j_s = slice(self.idx_s[1], self.idx_e[1] + 1) self.k_s = slice(self.idx_s[2], self.idx_e[2]) self.c_v = -dz / ((e[0] - s[0] + 1) * (e[1] - s[1] + 1)) def plot_3d(self, ax, alpha: float = 0.5): """Plot a brick and attach to an axis.""" X, Y, Z = np.meshgrid( [self.x_min, self.x_max], [self.y_min, self.y_max], [self.z_min, self.z_max], ) plot = partial(ax.plot_surface, alpha=alpha, color="#B0BF00") plot(X[:, :, 0], Y[:, :, 0], Z[:, :, 0]) plot(X[:, :, -1], Y[:, :, -1], Z[:, :, -1]) plot(X[:, 0, :], Y[:, 0, :], Z[:, 0, :]) plot(X[:, -1, :], Y[:, -1, :], Z[:, -1, :]) plot(X[0, :, :], Y[0, :, :], Z[0, :, :]) plot(X[-1, :, :], Y[-1, :, :], Z[-1, :, :]) def plot(self): """Plot.""" if self._plot: fig = plt.figure() ax = plt.subplot(111) ax.plot(self.captured) plt.show() def update(self): """Capture voltage.""" try: self.captured[self.grid.current_time_step] = self.c_v * np.sum( self.grid.E[self.i_s, self.j_s, self.k_s, self.direction.value]) except TypeError: self.captured = np.zeros(self.grid.n_steps) self.update()
[docs]class HFieldDetector(Detector): """Model of a magnetic field detector. Parameters ---------- x_min : float Minimum x coordinate of the bounding box containing the detector. y_min : float Minimum y coordinate of the bounding box containing the detector. z_min : float Minimum z coordinate of the bounding box containing the detector. x_max : float Maximum x coordinate of the bounding box containing the detector. y_max : float Maximum y coordinate of the bounding box containing the detector. z_max : float Maximum z coordinate of the bounding box containing the detector. name : Optional[str] Name of the source. plot : bool Flag to enable plotting. """ def attach_to_grid(self): """Attach object to grid.""" s = self.idx_s = ( np.argmin(np.abs(self.grid.x - self.x_min)), np.argmin(np.abs(self.grid.y - self.y_min)), np.argmin(np.abs(self.grid.z - self.z_min)), ) e = self.idx_e = ( np.argmin(np.abs(self.grid.x - self.x_max)), np.argmin(np.abs(self.grid.y - self.y_max)), np.argmin(np.abs(self.grid.z - self.z_max)), ) self.size = (e[0] - s[0] + 1, e[1] - s[1] + 1, e[2] - s[2] + 1) self.i_s = slice(self.idx_s[0], self.idx_e[0] + 1) self.j_s = slice(self.idx_s[1], self.idx_e[1] + 1) self.k_s = slice(self.idx_s[2], self.idx_e[2] + 1) logger.debug( f"EFieldDetector attached to {self.i_s}, {self.j_s} and {self.k_s}" ) def plot_3d(self, ax, alpha: float = 0.5): """Plot a brick and attach to an axis.""" X, Y, Z = np.meshgrid( [self.x_min, self.x_max], [self.y_min, self.y_max], [self.z_min, self.z_max], ) plot = partial(ax.plot_surface, alpha=alpha, color="#B0BF00") plot(X[:, :, 0], Y[:, :, 0], Z[:, :, 0]) plot(X[:, :, -1], Y[:, :, -1], Z[:, :, -1]) plot(X[:, 0, :], Y[:, 0, :], Z[:, 0, :]) plot(X[:, -1, :], Y[:, -1, :], Z[:, -1, :]) plot(X[0, :, :], Y[0, :, :], Z[0, :, :]) plot(X[-1, :, :], Y[-1, :, :], Z[-1, :, :]) def plot(self): """Plot.""" if not self._plot: return fig = plt.figure() ax = fig.add_subplot(111) logger.debug(f"shape of captured {self.captured.shape}") ax.plot(self.captured.flatten()) plt.show() def pos_processing(self): """Pos processing of results.""" self.values_time = self.captured.flatten() self.time = self.grid.dt * np.arange(0, self.grid.n_steps) # FFT: N = len(self.values_time) self.values_freq = np.fft.fft(self.values_time)[:N // 2] / N self.freq = np.fft.fftfreq(N, self.grid.dt)[:N // 2] def update(self): """Capture H Field.""" try: self.captured[:, :, :, self.grid.current_time_step] = self.grid.H[ self.i_s, self.j_s, self.k_s, self.direction.value] except TypeError: size = self.size + (self.grid.n_steps, ) self.captured = np.zeros_like(self.grid.H, shape=size) self.update()
[docs]class EFieldDetector(Detector): """Model of an electric field detector. Parameters ---------- x_min : float Minimum x coordinate of the bounding box containing the detector. y_min : float Minimum y coordinate of the bounding box containing the detector. z_min : float Minimum z coordinate of the bounding box containing the detector. x_max : float Maximum x coordinate of the bounding box containing the detector. y_max : float Maximum y coordinate of the bounding box containing the detector. z_max : float Maximum z coordinate of the bounding box containing the detector. name : Optional[str] Name of the source. plot : bool Flag to enable plotting. """ def attach_to_grid(self): """Attach object to grid.""" s = self.idx_s = ( np.argmin(np.abs(self.grid.x - self.x_min)), np.argmin(np.abs(self.grid.y - self.y_min)), np.argmin(np.abs(self.grid.z - self.z_min)), ) e = self.idx_e = ( np.argmin(np.abs(self.grid.x - self.x_max)), np.argmin(np.abs(self.grid.y - self.y_max)), np.argmin(np.abs(self.grid.z - self.z_max)), ) self.size = (e[0] - s[0] + 1, e[1] - s[1] + 1, e[2] - s[2] + 1) self.i_s = slice(self.idx_s[0], self.idx_e[0] + 1) self.j_s = slice(self.idx_s[1], self.idx_e[1] + 1) self.k_s = slice(self.idx_s[2], self.idx_e[2] + 1) logger.debug( f"EFieldDetector attached to {self.i_s}, {self.j_s} and {self.k_s}" ) def plot_3d(self, ax, alpha: float = 0.5): """Plot a brick and attach to an axis.""" X, Y, Z = np.meshgrid( [self.x_min, self.x_max], [self.y_min, self.y_max], [self.z_min, self.z_max], ) plot = partial(ax.plot_surface, alpha=alpha, color="#B0BF00") plot(X[:, :, 0], Y[:, :, 0], Z[:, :, 0]) plot(X[:, :, -1], Y[:, :, -1], Z[:, :, -1]) plot(X[:, 0, :], Y[:, 0, :], Z[:, 0, :]) plot(X[:, -1, :], Y[:, -1, :], Z[:, -1, :]) plot(X[0, :, :], Y[0, :, :], Z[0, :, :]) plot(X[-1, :, :], Y[-1, :, :], Z[-1, :, :]) def plot(self): """Plot.""" if not self._plot: return fig = plt.figure() ax = fig.add_subplot(111) logger.debug(f"shape of captured {self.captured.shape}") ax.plot(self.captured.flatten()) plt.show() def pos_processing(self): """Pos processing of results.""" self.values_time = self.captured.flatten() self.time = self.grid.dt * np.arange(0, self.grid.n_steps) N = len(self.values_time) self.values_freq = np.fft.fft(self.values_time)[:N // 2] / N self.freq = np.fft.fftfreq(N, self.grid.dt)[:N // 2] def update(self): """Capture E Field.""" try: self.captured[:, :, :, self.grid.current_time_step] = self.grid.E[ self.i_s, self.j_s, self.k_s, self.direction.value] except TypeError: size = self.size + (self.grid.n_steps, ) self.captured = np.zeros_like(self.grid.E, shape=size) self.update()