Source code for sources.smf

"""Single-Mode Fiber (SMF) Source Module

This module implements an extended Gaussian source that generates a ray bundle
correctly representing the output of a single-mode fiber (SMF). The source
produces rays with Gaussian distributions in both the spatial and angular
domains, using quasi-random Sobol sequences for high-quality sampling.

Key features:
    - Spatial profile defined by the Mode Field Diameter (MFD).
    - Angular profile defined by the 1/e² full divergence angle.
    - Non-paraxial direction cosines computed via tangent mapping.
    - Sobol quasi-random sampling for low-discrepancy ray sets.
    - Optional point-source mode (zero spatial extent).

Note on ``num_rays``:
    Sobol sequences require the sample count to be a power of two.  The
    ``generate_rays`` method automatically rounds the requested ``num_rays``
    **up** to the nearest power of two, so the returned ``RealRays`` object
    may contain more rays than requested.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from optiland import backend as be
from optiland.sources.base import BaseSource

if TYPE_CHECKING:
    from optiland.rays import RealRays


[docs] class SMFSource(BaseSource): """Extended Gaussian source representing a single-mode fiber output. This source generates rays with independent Gaussian distributions in both spatial (x, y) and angular (θ_x, θ_y) phase-space dimensions. It is designed to faithfully represent the far-field emission profile of a single-mode optical fiber. Sampling uses 4-dimensional Sobol sequences (two spatial, two angular) transformed to Gaussian distributions via the inverse error function. Direction cosines are computed non-paraxially from the sampled angles using the tangent mapping, ensuring physical accuracy at large divergence angles. Args: mfd_um (float): Mode Field Diameter in micrometers (µm). The 1/e² beam diameter at the fiber facet. The beam waist w₀ is ``mfd / 2``. divergence_deg_1e2 (float): Full-angle 1/e² far-field divergence in degrees. wavelength_um (float): Operating wavelength in micrometers (µm). total_power (float): Total optical power in Watts. Defaults to 1.0. position (tuple[float, float, float]): Source position (x, y, z) in millimeters. Defaults to ``(0, 0, 0)``. is_point_source (bool): If ``True``, spatial coordinates are set to zero (ideal point source). Defaults to ``False``. Attributes: wavelength (float): Wavelength in µm. total_power (float): Total power in W. mfd_um (float): Mode Field Diameter in µm. divergence_deg_1e2 (float): Full 1/e² divergence in degrees. sigma_spatial_mm (float): Spatial sampling sigma (mm), equal to w₀/2 converted to mm. sigma_angular_rad (float): Angular sampling sigma (rad), equal to half-angle / 2. is_point_source (bool): Whether to emit from a single point. Note: The actual number of rays returned by ``generate_rays`` is rounded up to the nearest power of two because Sobol sampling requires this. For example, requesting 1000 rays will generate 1024. """ def __init__( self, mfd_um: float, wavelength_um: float, divergence_deg_1e2: float | None = None, total_power: float = 1.0, position: tuple[float, float, float] = (0.0, 0.0, 0.0), is_point_source: bool = False, ): super().__init__(position=position) self.wavelength = wavelength_um self.total_power = total_power self.mfd_um = mfd_um self.is_point_source = is_point_source # If divergence is not provided, calculate it assuming a diffraction-limited # Gaussian beam: theta_half = wavelength / (pi * w0) if divergence_deg_1e2 is None: import math w0 = mfd_um / 2.0 theta_half_rad = wavelength_um / (math.pi * w0) self.divergence_deg_1e2 = 2 * math.degrees(theta_half_rad) else: self.divergence_deg_1e2 = divergence_deg_1e2 # Spatial sigma for importance sampling # w₀ is the 1/e² radius = MFD / 2 (in µm), convert to mm w0_um = mfd_um / 2.0 self.sigma_spatial_mm = (w0_um * 1e-3) / 2.0 # Angular sigma for importance sampling # Half-angle from full divergence, then divide by 2 for sigma import math theta_rad = math.radians(self.divergence_deg_1e2 / 2.0) self.sigma_angular_rad = theta_rad / 2.0
[docs] def generate_rays(self, num_rays: int) -> RealRays: """Generate rays from the SMF source using Sobol quasi-random sampling. Produces a ray bundle with Gaussian spatial and angular distributions appropriate for a single-mode fiber. Direction cosines are computed non-paraxially. Because Sobol sequences require a power-of-two sample count, the actual number of rays returned is ``2 ** ceil(log2(num_rays))``. Args: num_rays (int): Desired number of rays. Must be positive. Returns: RealRays: The generated ray bundle. Raises: ValueError: If ``num_rays`` is not a positive integer. """ if num_rays <= 0: raise ValueError("num_rays must be a positive integer.") from optiland.rays import RealRays # Round up to nearest power of 2 (required for Sobol sampling) num_samples = 1 << (num_rays - 1).bit_length() # 4-D Sobol: dim 0,1 → spatial (x, y), dim 2,3 → angular (θx, θy) u = be.sobol_sampler(dim=4, num_samples=num_samples, scramble=True) sqrt2 = be.sqrt(be.array(2.0)) # --- Spatial coordinates --- if self.is_point_source: x_start = be.zeros(num_samples) y_start = be.zeros(num_samples) else: x_start = self.sigma_spatial_mm * sqrt2 * be.erfinv(2 * u[:, 0] - 1) y_start = self.sigma_spatial_mm * sqrt2 * be.erfinv(2 * u[:, 1] - 1) # --- Angular coordinates (non-paraxial) --- theta_x = self.sigma_angular_rad * sqrt2 * be.erfinv(2 * u[:, 2] - 1) theta_y = self.sigma_angular_rad * sqrt2 * be.erfinv(2 * u[:, 3] - 1) # Convert angles to direction cosines via tangent mapping tau_x = be.tan(theta_x) tau_y = be.tan(theta_y) N_initial = 1.0 / be.sqrt(1.0 + tau_x**2 + tau_y**2) L_initial = tau_x * N_initial M_initial = tau_y * N_initial z_start = be.zeros_like(x_start) num_valid_rays = be.size(L_initial) # --- Power distribution (equal per ray, importance-sampled) --- power_per_ray = self.total_power / num_valid_rays intensity = be.full((num_valid_rays,), power_per_ray) wavelength_arr = be.full((num_valid_rays,), self.wavelength) rays = RealRays( x=x_start, y=y_start, z=z_start, L=L_initial, M=M_initial, N=N_initial, intensity=intensity, wavelength=wavelength_arr, ) # Transform from local source coordinates to global coordinates self.cs.globalize(rays) return rays
def __repr__(self) -> str: """Return a string representation of the SMFSource.""" position = (float(self.cs.x), float(self.cs.y), float(self.cs.z)) mode = "point" if self.is_point_source else "extended" return ( f"SMFSource(mfd={self.mfd_um}µm, " f"divergence={self.divergence_deg_1e2}°, " f"wavelength={self.wavelength}µm, " f"power={self.total_power}W, " f"mode={mode}, " f"position={position})" )