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(Literal['point_dipole']) -
center_um(Vec3Um) -
polarization(FieldComponentName) -
amplitude(float) -
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:
-
type(Literal['plane_wave']) -
axis(AxisName) -
direction(DirectionName) -
position_um(float) -
polarization(FieldComponentName) -
amplitude(float) -
source_time(SourceTimeType) -
angle_theta_rad(Optional[float]) -
angle_phi_rad(Optional[float])
Validators:
-
_electric_only→polarization -
_polarization_tangential
model_config
class-attribute
instance-attribute
¶
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:
-
type(Literal['mode_source']) -
axis(AxisName) -
direction(DirectionName) -
position_um(float) -
polarization(FieldComponentName) -
amplitude(float) -
n_eff(float) -
nu(int) -
nv(int) -
profile(Tuple[float, ...]) -
minor_polarization(Optional[FieldComponentName]) -
profile_minor(Optional[Tuple[float, ...]]) -
profile_h(Optional[Tuple[float, ...]]) -
profile_h_minor(Optional[Tuple[float, ...]]) -
freqs_hz(Optional[Tuple[FreqHz, ...]]) -
n_eff_by_freq(Optional[Tuple[float, ...]]) -
profiles_by_freq(Optional[Tuple[Tuple[float, ...], ...]]) -
profiles_minor_by_freq(Optional[Tuple[Tuple[float, ...], ...]]) -
profiles_h_by_freq(Optional[Tuple[Tuple[float, ...], ...]]) -
profiles_h_minor_by_freq(Optional[Tuple[Tuple[float, ...], ...]]) -
source_time(SourceTimeType) -
mode_solve(Optional[ModeSolveProvenance])
Validators:
-
_electric_only→polarization,minor_polarization -
_check
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:
-
solver(Literal['yee']) -
polarization(Literal['TE', 'TM']) -
mode_index(int) -
wavelength_um(PositiveUm) -
center_um(Tuple[float, float]) -
size_um(Tuple[PositiveUm, PositiveUm]) -
dl_um(PositiveUm) -
supersample(int) -
num_modes(Optional[int]) -
num_freqs(int) -
input_sha256(str)
Validators:
-
_mode_count_covers_index
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:
-
type(Literal['gaussian_pulse']) -
freq0_hz(float) -
fwidth_hz(float) -
offset(float) -
phase(float) -
band_freqs_hz(Optional[Tuple[FreqHz, ...]]) -
carrier_index(Optional[int])
Validators:
-
_check_broadband_carrier
source_end_time_s
property
¶
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
¶
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
¶
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
¶
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(Literal['cw']) -
freq0_hz(float) -
ramp_cycles(float) -
phase(float)