Skip to content

Sources

Excitations and their time dependence. Every source carries a source_time: a GaussianPulse for broadband runs or CW for single-frequency steady state.

Field sources

PointDipole pydantic-model

Bases: FrozenModel

Soft point current source at the nearest Yee node. An ELECTRIC polarization (Ex/Ey/Ez) injects an electric current density onto E after the E-update; a MAGNETIC polarization (Hx/Hy/Hz) injects a magnetic current density onto H right after the H-update. amplitude is the peak current density J0 [A/m^2] (electric) or M0 [V/m^2] (magnetic); the injected current MOMENT is J0 * dl^3 on a uniform grid, so the Hertzian radiated power of an electric dipole in vacuum is eta0 * k^2 * (J0*dl^3)^2 / (12*pi). The engine normalizes every DFT/flux output per unit amplitude of the FIRST wire-order source and RunResult multiplies that amplitude back in, so the reported spectra are those of the sources as declared (fields scale with amplitude, flux with its square). A dipole placed inside the PML/absorber layers is NOT rejected by the engine and radiates almost nothing: keep sources inside the interior (Simulation.check_runnable refuses such a run; see Simulation.sources_in_boundary_layers).

Magnetic dipoles are supported on both the CPU reference solver and the GPU.

Fields:

type pydantic-field

type: Literal['point_dipole'] = 'point_dipole'

center_um pydantic-field

center_um: Vec3Um

polarization pydantic-field

polarization: FieldComponentName

amplitude pydantic-field

amplitude: float = 1.0

source_time pydantic-field

source_time: SourceTimeType

PlaneWave pydantic-model

Bases: FrozenModel

Normal-incidence plane wave injected on a TF/SF plane perpendicular to axis at position_um, fed by a dispersion-matched 1-D auxiliary solver. amplitude is the peak incident E-field E0 in V/m. Both transverse axes must be periodic or Bloch (enforced by Simulation), and the plane must not intersect PML (checked by phsolver validate).

Config:

  • json_schema_extra: _PLANE_WAVE_TANGENTIAL

Fields:

Validators:

model_config class-attribute instance-attribute

model_config = ConfigDict(
    json_schema_extra=_PLANE_WAVE_TANGENTIAL
)

type pydantic-field

type: Literal['plane_wave'] = 'plane_wave'

axis pydantic-field

axis: AxisName

direction pydantic-field

direction: DirectionName

position_um pydantic-field

position_um: float

polarization pydantic-field

polarization: FieldComponentName

amplitude pydantic-field

amplitude: float = 1.0

source_time pydantic-field

source_time: SourceTimeType

angle_theta_rad pydantic-field

angle_theta_rad: Optional[float] = None

angle_phi_rad pydantic-field

angle_phi_rad: Optional[float] = None

angle_theta property

angle_theta: Optional[float]

Deprecated spelling of angle_theta_rad.

angle_phi property

angle_phi: Optional[float]

Deprecated spelling of angle_phi_rad.

TFSFBox module-attribute

TFSFBox = TfsfBox

Mode sources

ModeSource pydantic-model

Bases: FrozenModel

Inject a guided mode on a TF/SF plane.

Deprecated

The aux-line ModeSource is deprecated in favour of the source current Huygens launch (per-cell PointDipole source planes via mode_launch over a discrete Yee mode). Both paths support uniform and graded transverse/propagation axes; the aux-line source uses the graded auxiliary-line construction. The source-current path is preferred for solved Yee modes because it carries their discrete paired-H fields and grid provenance. The auxiliary-line path remains for the adjoint (gradient pinned to it) and scalar/FLM modes; new solved-mode code should use mode_launch.

The 1-D auxiliary line runs at the mode phase index n_eff (which gives both the modal phase velocity and the scalar-limit modal impedance), and each transverse plane point is scaled by profile, the FDE eigenmode resampled onto the grid's transverse plane, row-major [v*nu + u] with u/v the lower/higher-indexed transverse axes. polarization is the major tangential E component. Build via photonhub.analysis.mode_devices.mode_source (it computes profile/ n_eff from an FDE mode); hand-construction is rarely needed. Unlike the plane wave, the injection plane MAY cut through structures.

Full-vector injection (schema 1.8.0, additive/optional). A full-vector FDE mode (VectorMode) carries BOTH transverse-E components. When the builder ships them, minor_polarization names the second tangential-E component and profile_minor is its real signed profile (same row-major [v*nu + u] layout, sampled onto the same plane, scaled consistently with profile so the two carry the mode's true component RATIO). For a lossless guided mode both transverse-E components are co-real (relative phase 0 or π), so a single real signed profile is exact; the engine injects each off the same guided-mode aux carrier with its own scalar-limit paired H. When the minor fields are omitted (the legacy default) the engine injects the scalar-limit major component only, older engines/JSON stay valid.

Fields:

Validators:

type pydantic-field

type: Literal['mode_source'] = 'mode_source'

axis pydantic-field

axis: AxisName

direction pydantic-field

direction: DirectionName

position_um pydantic-field

position_um: float

polarization pydantic-field

polarization: FieldComponentName

amplitude pydantic-field

amplitude: float = 1.0

n_eff pydantic-field

n_eff: float

nu pydantic-field

nu: int

nv pydantic-field

nv: int

profile pydantic-field

profile: Tuple[float, ...]

minor_polarization pydantic-field

minor_polarization: Optional[FieldComponentName] = None

profile_minor pydantic-field

profile_minor: Optional[Tuple[float, ...]] = None

profile_h pydantic-field

profile_h: Optional[Tuple[float, ...]] = None

profile_h_minor pydantic-field

profile_h_minor: Optional[Tuple[float, ...]] = None

freqs_hz pydantic-field

freqs_hz: Optional[Tuple[FreqHz, ...]] = None

n_eff_by_freq pydantic-field

n_eff_by_freq: Optional[Tuple[float, ...]] = None

profiles_by_freq pydantic-field

profiles_by_freq: Optional[
    Tuple[Tuple[float, ...], ...]
] = None

profiles_minor_by_freq pydantic-field

profiles_minor_by_freq: Optional[
    Tuple[Tuple[float, ...], ...]
] = None

profiles_h_by_freq pydantic-field

profiles_h_by_freq: Optional[
    Tuple[Tuple[float, ...], ...]
] = None

profiles_h_minor_by_freq pydantic-field

profiles_h_minor_by_freq: Optional[
    Tuple[Tuple[float, ...], ...]
] = None

source_time pydantic-field

source_time: SourceTimeType

mode_solve pydantic-field

mode_solve: Optional[ModeSolveProvenance] = None

ModeSolveProvenance pydantic-model

Bases: FrozenModel

Reproducible authoring recipe for a solved ModeSource.

This optional block has no native numerical semantics: the engine consumes the resolved arrays beside it. Workbench uses it to restore the compact port editor after reopen and to detect when geometry/grid edits have made those arrays stale. input_sha256 is a conservative hash of the canonical solve inputs; legacy sources without this block remain valid but have unverified profile provenance. polarization names the Yee solver's natural horizontal/vertical family. A linked modal port maps that family to physical width/thickness TE/TM using the port's thickness_axis.

Fields:

Validators:

  • _mode_count_covers_index

solver pydantic-field

solver: Literal['yee'] = 'yee'

polarization pydantic-field

polarization: Literal['TE', 'TM']

mode_index pydantic-field

mode_index: int

wavelength_um pydantic-field

wavelength_um: PositiveUm

center_um pydantic-field

center_um: Tuple[float, float]

size_um pydantic-field

size_um: Tuple[PositiveUm, PositiveUm]

dl_um pydantic-field

dl_um: PositiveUm

supersample pydantic-field

supersample: int

num_modes pydantic-field

num_modes: Optional[int] = None

num_freqs pydantic-field

num_freqs: int = 1

input_sha256 pydantic-field

input_sha256: str

Time dependence

GaussianPulse pydantic-model

Bases: FrozenModel

J(t) = amplitude * exp(-(t-t0)^2/(2 tau^2)) * cos(2 pi freq0 (t-t0) + phase), tau = 1/(2 pi fwidth), t0 = offset * tau.

The frequency-domain envelope is a Gaussian whose standard deviation in Hz is exactly fwidth_hz. Two facts drive source tuning: the run cannot auto-shutoff before the source stops injecting at (offset+8)*tau, so settling scales as 1/fwidth; and too broad a pulse puts spectral weight on DC and goes unstable. The for_band constructor tunes freq0_hz/fwidth_hz for both, and source_end_time_s / dc_amplitude expose them for auditing.

Fields:

Validators:

  • _check_broadband_carrier

type pydantic-field

type: Literal['gaussian_pulse'] = 'gaussian_pulse'

freq0_hz pydantic-field

freq0_hz: float

fwidth_hz pydantic-field

fwidth_hz: float

offset pydantic-field

offset: float = 5.0

phase pydantic-field

phase: float = 0.0

band_freqs_hz pydantic-field

band_freqs_hz: Optional[Tuple[FreqHz, ...]] = None

carrier_index pydantic-field

carrier_index: Optional[int] = None

tau_s property

tau_s: float

Gaussian width in seconds, 1/(2 pi fwidth_hz) (engine pulse_tau).

t0_s property

t0_s: float

Pulse-centre time, offset * tau (engine t0).

source_end_time_s property

source_end_time_s: float

Instant the source is considered done injecting, (offset+8)*tau (engine source_end_time). Auto-shutoff may not terminate the run before this time, so it is the settling floor on the step count, inversely proportional to fwidth_hz.

dc_amplitude property

dc_amplitude: float

Relative spectral weight the pulse places on DC, |S(0)| / |S(freq0)| of the analytic TWO-sideband spectrum (engine gaussian_pulse_spectrum): at f = 0 the +freq0 and -freq0 sidebands contribute equally and interfere through the carrier phase, so

|S(0)|/|S(freq0)| = 2*|cos(phase)| * exp(-(freq0/fwidth)^2 / 2)

(spectral_amplitude alone is the dominant +freq0 sideband, which under-reports the DC weight by the 2*|cos(phase)| factor, 2x at the default phase=0). A large value is the over-broadband instability tell (even a few percent is risky); for_band keeps this far below 1.

spectral_amplitude

spectral_amplitude(freq_hz: float) -> float

Relative spectral envelope at freq_hz (peak 1.0 at freq0_hz): exp(-(freq-freq0)^2/(2 fwidth^2)), the dominant sideband of the analytic pulse spectrum (engine gaussian_pulse_spectrum).

for_band classmethod

for_band(
    *,
    freqs_hz: Optional[Sequence[float]] = None,
    wlens_um: Optional[Sequence[float]] = None,
    freq0_hz: Optional[float] = None,
    wlen0_um: Optional[float] = None,
    band_sigmas: float = 1.0,
    offset: float = 5.0,
    phase: float = 0.0,
    min_dc_sigmas: float = 4.0,
) -> GaussianPulse

Pick freq0_hz/fwidth_hz to cover a measurement band with the SHORTEST settling that band justifies.

Exactly one of freqs_hz or wlens_um defines the band (only its extremes matter). freq0_hz (or its wavelength form wlen0_um, microns) defaults to the band centre (fmin+fmax)/2. fwidth_hz is set so the further band edge sits at band_sigmas standard deviations from freq0: band_sigmas=1 (default) puts the edges at ~61% spectral amplitude, the broadest pulse, hence the shortest source_end_time_s, that covers the band near-flat without spilling much energy outside it. Lower band_sigmas broadens the pulse further (settling shrinks, the band flattens toward the peak) at the cost of spectral density spread outside the band (lower in-band SNR) and DC margin; higher band_sigmas narrows it, concentrating spectral density near freq0 (higher central SNR) but rolling off the band edges and settling slower.

fwidth is capped at freq0 / min_dc_sigmas so DC stays at least min_dc_sigmas (default 4 → DC weight 2*exp(-8) ~ 6.7e-4 at phase 0, both sidebands counted, see dc_amplitude) standard deviations from freq0; past this the broadband drive dumps DC into its lowest carrier and the run goes unstable. A band too wide for that cap is clamped (narrower fwidth, longer settling, under-driven edges) with a warning, such a band wants a broadband mode source (num_freqs), not a single Gaussian.

Returns a GaussianPulse; read source_end_time_s on it to see the resulting settling floor.

plot

plot(*, ax=None)

Preview the injected current J(t) (envelope × carrier) and, when building its own figure, the spectral envelope |J(f)| alongside, the quick check that the pulse covers your band and settles. Returns the time-domain matplotlib Axes. See photonhub.viz.plot_source_time.

CW pydantic-model

Bases: FrozenModel

Continuous wave (schema 1.18): J(t) = amplitude * ramp(t) * cos(2 pi freq0 t + phase) with a C¹ smooth sin² turn-on over ramp_cycles carrier periods, then constant forever. For steady-state analysis: the source never ends, so auto-shutoff never fires, set run_time_s long enough to ring up.

The source-spectrum normalization is defined AT THE CARRIER only: when a CW is the normalization source (first in wire order), every DFT/flux monitor frequency must equal freq0_hz (phsolver validate enforces); the normalized phasors then converge to the steady-state response per unit drive as the run lengthens. CPU reference solver only in this release.

Fields:

type pydantic-field

type: Literal['cw'] = 'cw'

freq0_hz pydantic-field

freq0_hz: float

ramp_cycles pydantic-field

ramp_cycles: float = 10.0

phase pydantic-field

phase: float = 0.0

band_freqs_hz property

band_freqs_hz

Return None. A continuous wave has no broadband carrier window.