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 Tuple, Dict, Optional, Union, Literal
import numpy as np

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


[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