Source code for prismo.sources.plane_wave

"""
Plane wave sources for FDTD simulations.

This module implements plane wave sources for exciting uniform electromagnetic
fields propagating in a specified direction.
"""

from typing import Literal, Optional

from prismo.core.fields import ElectromagneticFields, FieldComponent
from prismo.core.grid import YeeGrid
from prismo.sources.base import Source
from prismo.sources.waveform import ContinuousWave, GaussianPulse


[docs] class PlaneWaveSource(Source): """ Plane wave source for exciting uniform electromagnetic fields. Parameters ---------- center : Tuple[float, float, float] Physical coordinates of the wave center (x, y, z) in meters. size : Tuple[float, float, float] Physical dimensions of the source region (Lx, Ly, Lz) in meters. direction : Literal["x", "y", "z", "+x", "-x", "+y", "-y", "+z", "-z"] Propagation direction of the wave, with optional sign. polarization : Literal["x", "y", "z"] Polarization direction of the electric field (must be perpendicular to direction). frequency : float Center frequency in Hz. pulse : bool, optional Whether to use a Gaussian pulse (True) or continuous wave (False), default=True. pulse_width : float, optional Width of the Gaussian pulse in seconds, required if pulse=True. amplitude : float, optional Peak amplitude of the source, default=1.0. phase : float, optional Phase offset in radians, default=0.0. name : str, optional Name of the source for identification. enabled : bool, optional Flag to enable/disable the source, default=True. """
[docs] def __init__( self, center: tuple[float, float, float], size: tuple[float, float, float], direction: str, polarization: Literal["x", "y", "z"], frequency: float, pulse: bool = True, pulse_width: Optional[float] = None, amplitude: float = 1.0, phase: float = 0.0, name: Optional[str] = None, enabled: bool = True, ): super().__init__(center=center, size=size, name=name, enabled=enabled) # Parse direction and sign self._parse_direction(direction) # Validate direction and polarization self._validate_direction_polarization(self.direction, polarization) self.polarization = polarization self.frequency = frequency self.wavelength = 299792458.0 / frequency # c / f # Create waveform based on parameters if pulse: if pulse_width is None: raise ValueError("pulse_width must be provided for pulsed sources") self.waveform = GaussianPulse( frequency=frequency, pulse_width=pulse_width, amplitude=amplitude, phase=phase, ) else: self.waveform = ContinuousWave( frequency=frequency, amplitude=amplitude, phase=phase ) # Will be computed when initialized self._e_components: dict[str, FieldComponent] = {} self._h_components: dict[str, FieldComponent] = {}
def _parse_direction(self, direction: str) -> None: """ Parse the direction string to extract direction and sign. Parameters ---------- direction : str Propagation direction with optional sign. """ if direction.startswith("+"): self.direction = direction[1:] self.direction_sign = 1 elif direction.startswith("-"): self.direction = direction[1:] self.direction_sign = -1 else: self.direction = direction self.direction_sign = 1 # Validate direction if self.direction.lower() not in ["x", "y", "z"]: raise ValueError( f"Invalid direction: {direction}. Must be one of: x, y, z, +x, -x, +y, -y, +z, -z" ) def _validate_direction_polarization( self, direction: str, polarization: str ) -> None: """ Validate that direction and polarization are perpendicular. Parameters ---------- direction : str Propagation direction ("x", "y", or "z"). polarization : str Electric field polarization ("x", "y", or "z"). Raises ------ ValueError If direction and polarization are the same. """ if direction.lower() == polarization.lower(): raise ValueError( f"Polarization ({polarization}) must be perpendicular to " f"propagation direction ({direction})" )
[docs] def initialize(self, grid: YeeGrid) -> None: """ Initialize the plane wave source on a specific grid. Parameters ---------- grid : YeeGrid The grid on which to initialize the source. """ super().initialize(grid) # Determine field components based on direction and polarization self._setup_field_components()
def _setup_field_components(self) -> None: """ Set up the field components for the plane wave. This determines which field components to update based on the propagation direction and polarization. """ # Define the components to update based on direction and polarization direction_map = { # For x-propagation ("x", "y"): {"E": ["Ey"], "H": ["Hz"]}, ("x", "z"): {"E": ["Ez"], "H": ["Hy"]}, # For y-propagation ("y", "x"): {"E": ["Ex"], "H": ["Hz"]}, ("y", "z"): {"E": ["Ez"], "H": ["Hx"]}, # For z-propagation ("z", "x"): {"E": ["Ex"], "H": ["Hy"]}, ("z", "y"): {"E": ["Ey"], "H": ["Hx"]}, } # Get components based on direction and polarization key = (self.direction.lower(), self.polarization.lower()) if key not in direction_map: raise ValueError( f"Invalid direction ({self.direction}) and polarization ({self.polarization}) combination" ) self._e_components = direction_map[key]["E"] self._h_components = direction_map[key]["H"]
[docs] def update_fields( self, fields: ElectromagneticFields, time: float, dt: float ) -> None: """ Update electromagnetic fields with plane wave source contribution. Parameters ---------- fields : ElectromagneticFields Electromagnetic fields to update. time : float Current simulation time in seconds. dt : float Time step in seconds. """ if not self.enabled or self._grid is None: return # Get waveform value at current time amplitude = self.waveform(time) # Determine the appropriate field components to update for comp in self._e_components: # Get source region indices indices = self._source_region[comp] # Apply source with uniform amplitude field_component = fields[comp] field_component[indices] += amplitude * self.direction_sign # Calculate corresponding H field components for comp in self._h_components: # Get source region indices indices = self._source_region[comp] # Apply source with uniform amplitude (E/Z₀) field_component = fields[comp] field_component[indices] += (amplitude / 377.0) * self.direction_sign