physicskit.fields#
Classical and quantum field theory: electrodynamics, solitons, and BEC vortex lattices.
Typical usage:
import physicskit as pk
import numpy as np
# A dark soliton on a defocusing NLS background:
x = np.linspace(-40, 40, 1024)
psi0 = pk.fields.nls_dark_soliton(x, t=0.0)
- physicskit.fields.animate_casimir_modes(d_values, c=1.0, n_show=12, interval=150)[source]#
Animate the discrete cavity mode spectrum and Casimir energy as the plate separation
dis swept.Left panel: the first
n_showdiscrete mode frequencies (physicskit.fields.quantum_fields.casimir_mode_frequencies()) as a stem plot, against the continuum they approach at larged(dashed line of slope \(\pi c\)) – visibly denser (closer to the continuum) at large separation. Right panel: the regularized Casimir energy (physicskit.fields.quantum_fields.casimir_energy_1d()) as a curve over the full sweep, with a marker tracking the currentd.- Parameters:
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> anim = animate_casimir_modes(np.linspace(1.0, 5.0, 5)) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.animate_density_2d(frames, extent=None, times=None, interval=50, cmap='viridis', ax=None)[source]#
Animate a sequence of non-negative 2D density snapshots (e.g. BEC or flux-tube energy density) as an imshow heatmap.
- Parameters:
frames (
ndarray) – Snapshots. Complex frames (a wavefunction) are converted to \(|\psi|^2\); real frames are used directly as the density/energy map.extent (
tuple|None) –(xmin, xmax, ymin, ymax)passed toimshow; defaults to pixel indices.times (
ndarray|None) – Time (or other sweep parameter, e.g. propagation distance) of each frame.interval (
int) – Delay between frames, in milliseconds.cmap (
str) – Colormap.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> from physicskit.fields.quantum_fields import harmonic_trap_grid, gpe_imprint_vortex, gpe_evolve >>> n, length = 32, 12.0 >>> X, Y, KX, KY, K2 = harmonic_trap_grid(n, length) >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi0 = gpe_imprint_vortex(np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex), X, Y, [(1.0, 0.0)]) >>> frames, times = gpe_evolve(psi0, V, g=2.0, dt=1e-3, steps=20, K2=K2, snapshot_stride=5) >>> anim = animate_density_2d(frames, times=times) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.animate_field_1d(x, frames, times=None, interval=50, ylabel='u(x, t)', ax=None)[source]#
Animate a sequence of 1D field snapshots as a line plot.
Shared by the KdV, NLS, and Sine-Gordon evolvers: feed it the frames from
physicskit.fields.solitons.kdv_evolve_frames(),nls_evolve_frames(), orsine_gordon_evolve_frames().- Parameters:
x (
ndarray) – Spatial grid.frames (
ndarray) – Field snapshots. Complex frames (an NLS wavefunction) are plotted as \(|\psi|\); real frames (KdV, Sine-Gordon) are plotted directly.times (
ndarray|None) – Time of each frame, shown in the title.interval (
int) – Delay between frames, in milliseconds.ylabel (
str) – Y-axis label.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> from physicskit.fields.solitons import kdv_soliton, kdv_evolve_frames >>> x = np.linspace(-30, 30, 256, endpoint=False) >>> frames, times = kdv_evolve_frames(kdv_soliton(x, c=4.0, x0=-15), x, dt=0.001, steps_per_frame=50, n_frames=3) >>> anim = animate_field_1d(x, frames, times) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.animate_field_2d(X, Y, frames, times=None, interval=50, cmap='RdBu_r', ax=None)[source]#
Animate a sequence of 2D scalar field snapshots (e.g. FDTD
Ez) as an imshow heatmap.Uses a diverging colormap centered at zero, symmetric about the largest-magnitude value across all frames – appropriate for an oscillating field like
Ez, unlike the non-negative densities handled byanimate_density_2d().- Parameters:
X (
ndarray) – Real-space coordinate grids (used only for their extent).Y (
ndarray) – Real-space coordinate grids (used only for their extent).frames (
ndarray) – Field snapshots, e.g. fromphysicskit.fields.electrodynamics.fdtd_2d_tmz_evolve().times (
ndarray|None) – Time of each frame, shown in the title.interval (
int) – Delay between frames, in milliseconds.cmap (
str) – Diverging colormap.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> from physicskit.fields.electrodynamics import ( ... courant_limit_2d, fdtd_2d_tmz_evolve, oscillating_dipole_source) >>> Nx, Ny = 30, 30 >>> x = np.arange(Nx) * 1e-3 >>> y = np.arange(Ny) * 1e-3 >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> Ez0 = Hx0 = Hy0 = np.zeros((Nx, Ny)) >>> eps_r = mu_r = np.ones((Nx, Ny)) >>> dt = 0.5 * courant_limit_2d(1e-3, 1e-3) >>> source = oscillating_dipole_source(Nx // 2, Ny // 2, amplitude=1.0, freq=5e10) >>> frames, times = fdtd_2d_tmz_evolve(Ez0, Hx0, Hy0, eps_r, mu_r, steps=10, dt=dt, dx=1e-3, dy=1e-3, source=source, snapshot_stride=2) >>> anim = animate_field_2d(X, Y, frames, times) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.animate_flux_tube(shape, dx, dy, separations, flux_quantum=1.0, interval=80, ax=None)[source]#
Animate the toy confinement flux tube stretching as two charges are pulled apart.
Builds a frame for each separation in
separationsviaphysicskit.fields.electrodynamics.flux_tube_energy_density_2d()and draws it as a heatmap with two markers tracking the charge positions, so the confined-energy “tube” visibly stretches between them.- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.dx (
float) – Grid spacing.dy (
float) – Grid spacing.separations (
ndarray) – Sequence of charge separations to sweep over (the animation’s “time” axis).flux_quantum (
float) – Charge magnitude, as influx_tube_field_1d().interval (
int) – Delay between frames, in milliseconds.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> anim = animate_flux_tube((120, 30), dx=0.2, dy=0.2, separations=np.linspace(4.0, 16.0, 5)) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.casimir_energy_1d(d, c=1.0, cutoff=None)[source]#
Regularized zero-point (Casimir) energy of the discrete 1D cavity spectrum.
Uses the standard exponential-cutoff regularization: the sum \(\sum_n n x^n = x/(1-x)^2\) (with \(x=e^{-a}\), \(a=\pi c\,\text{cutoff}/d\)) has an exact closed form, so no series truncation is needed. Its small-
cutoff(small-a) expansion is \(\tfrac{1}{4\sinh^2(a/2)} = 1/a^2 - 1/12 + O(a^2)\); the \(1/a^2\) piece is the (unphysical, cutoff-scheme-dependent) divergence that a continuum reference calculation regularized the same way would also produce, so it is subtracted exactly, leaving the finite remainder that survives ascutoff -> 0:\[E(d) \to -\frac{\pi c}{24 d}\]the standard 1D massless-field Casimir energy (equivalently, the Casimir energy of a CFT strip with central charge 1).
- Parameters:
d (
float) – Plate separation.c (
float) – Wave speed.cutoff (
float|None) – Regulator scale. Defaults to1e-3 * d / c. This subtraction is a difference of two large, nearly-equal floating-point terms (\(\propto 1/\text{cutoff}^2\)), so makingcutofftoo small loses precision to cancellation rather than gaining it;1e-4*d/cto1e-2*d/cis the well-behaved range in double precision.
- Return type:
- Returns:
float – The regularized (finite, cutoff-subtracted) zero-point energy.
See also
casimir_mode_frequenciesThe (bare, un-regularized) mode spectrum being summed.
Examples
The cutoff-and-subtract result agrees with the known closed form \(-\pi c/(24d)\), and (within the well-behaved cutoff range noted above) agrees more closely as the cutoff shrinks:
>>> d = 3.0 >>> exact = -np.pi * 1.0 / (24 * d) >>> bool(abs(casimir_energy_1d(d, cutoff=3e-3 * d) - exact) < 1e-6) True >>> bool(abs(casimir_energy_1d(d, cutoff=7e-4 * d) - exact) < 1e-8) True
The energy grows less negative (weaker confinement of vacuum energy) as the plates separate, giving an attractive force \(-dE/dd < 0\):
>>> bool(casimir_energy_1d(1.0) < casimir_energy_1d(2.0) < 0) True
- physicskit.fields.casimir_mode_frequencies(d, c=1.0, n_max=200)[source]#
Discrete standing-wave mode frequencies of a 1D cavity of plate separation
d.- Parameters:
- Return type:
- Returns:
ndarray, shape (n_max,) – \(\omega_n = n\pi c/d\) for \(n=1,\dots,n_{max}\).
See also
casimir_energy_1dThe regularized zero-point energy of this mode spectrum.
Examples
>>> omega = casimir_mode_frequencies(d=2.0, c=1.0, n_max=3) >>> [round(float(w), 4) for w in omega] [1.5708, 3.1416, 4.7124]
- physicskit.fields.count_vortices(psi, density_threshold=0.05)[source]#
Locate quantized vortices by summing the phase winding around each grid plaquette.
Plaquettes in very low density regions are excluded, since the phase of a near-zero-amplitude wavefunction is dominated by numerical noise and produces spurious windings there.
A vortex core that sits exactly on a grid vertex (rather than strictly inside a plaquette) can go undetected, since its circulation is then split ambiguously between the four plaquettes touching that vertex; in practice this is avoided by seeding vortices at positions not exactly on the grid.
- Parameters:
- Return type:
- Returns:
ndarray of int, shape (n, n) – Integer winding number around each plaquette’s corner
(i, j); nonzero entries mark a vortex core (+1or-1for a singly-quantized vortex/antivortex) enclosed by that plaquette.
Examples
>>> import numpy as np >>> X, Y, KX, KY, K2 = harmonic_trap_grid(48, 10.0) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi_vortex = gpe_imprint_vortex(psi0, X, Y, [(0.0, 0.0)]) >>> winding = count_vortices(psi_vortex) >>> int(np.sum(np.abs(winding))) 1
- physicskit.fields.courant_limit_1d(dx)[source]#
Maximum stable time step for the 1D FDTD update.
- Parameters:
dx (
float) – Spatial grid spacing in meters.- Return type:
- Returns:
float – The Courant stability limit \(\Delta t = \Delta x / c_0\).
Examples
>>> round(float(courant_limit_1d(1e-3) * C0 / 1e-3), 6) 1.0
- physicskit.fields.courant_limit_2d(dx, dy)[source]#
Maximum stable time step for the 2D FDTD update.
- Parameters:
- Return type:
- Returns:
float – The Courant stability limit \(\Delta t = 1 / (c_0 \sqrt{1/\Delta x^2 + 1/\Delta y^2})\).
Examples
>>> dt = courant_limit_2d(1e-3, 1e-3) >>> round(float(dt * C0 / 1e-3), 6) 0.707107
- physicskit.fields.dielectric_slab(shape, i_start, i_end, eps_r_slab=4.0)[source]#
A planar dielectric slab:
eps_r_slabbetween grid columnsi_startandi_end, vacuum elsewhere.A minimal demo permittivity map for
fdtd_2d_tmz()/fdtd_2d_tmz_evolve(): a wave launched from one side crossingi_startpartially reflects and partially refracts/transmits into the slab, and again ati_endon the way out, illustrating propagation through a medium interface.- Parameters:
- Return type:
- Returns:
ndarray, shape (Nx, Ny) –
eps_rmap:eps_r_slabinside the slab,1.0outside.
Examples
>>> eps_r = dielectric_slab((40, 20), i_start=15, i_end=25, eps_r_slab=4.0) >>> float(eps_r[10, 5]), float(eps_r[20, 5]) (1.0, 4.0)
- physicskit.fields.fdtd_1d(Ez0, Hy0, eps_r, mu_r, steps, dt, dx, sigma=None)[source]#
Evolve a 1D plane wave (Ez, Hy) on a Yee grid using leapfrog FDTD.
Discretizes Maxwell’s curl equations for fields varying only along
x, \(\partial_t H_y = \partial_x E_z/\mu\) and \(\partial_t E_z = \partial_x H_y/\epsilon\) (the same signs asfdtd_2d_tmz()), so a wave moving toward+xhas \(H_y = -E_z/\eta\) (Poynting vector \(-E_zH_y>0\)).- Parameters:
Ez0 (
ndarray) – Initial electric field.Hy0 (
ndarray) – Initial magnetic field, staggered half a cell ahead ofEz0.eps_r (
ndarray) – Relative permittivity and permeability at eachEzgrid point.mu_r (
ndarray) – Relative permittivity and permeability at eachEzgrid point.steps (
int) – Number of time steps to advance.dt (
float) – Time step in seconds; must satisfycourant_limit_1d().dx (
float) – Spatial grid spacing in meters.sigma (
ndarray|None) – Electric conductivity (S/m) at each grid point, e.g. frompml_conductivity_profile(), for a lossy/absorbing boundary. Defaults to a lossless grid with hard (PEC) walls at both ends.
- Return type:
- Returns:
Ez, Hy (ndarray) – Fields at the final time step, same shapes as
Ez0,Hy0.
See also
fdtd_2d_tmzThe 2D (TMz-mode) analog of this solver.
Examples
A smooth Gaussian pulse, launched with the impedance-matched initial condition of a purely right-moving wave (\(H_y=-E_z/\eta_0\)), propagates at exactly the vacuum speed of light:
>>> import numpy as np >>> N = 800 >>> dx = 1e-3 >>> dt = 0.99 * courant_limit_1d(dx) >>> eta0 = np.sqrt(MU0 / EPS0) >>> x0, sigma_pulse = 100, 25 >>> Ez0 = np.exp(-((np.arange(N) - x0) ** 2) / (2 * sigma_pulse ** 2)) >>> xh = np.arange(N - 1) + 0.5 >>> Hy0 = -np.exp(-((xh - x0) ** 2) / (2 * sigma_pulse ** 2)) / eta0 >>> eps_r, mu_r = np.ones(N), np.ones(N) >>> Ez, Hy = fdtd_1d(Ez0, Hy0, eps_r, mu_r, steps=360, dt=dt, dx=dx) >>> peak = np.argmax(Ez) >>> measured_speed = (peak - x0) * dx / (360 * dt) >>> round(float(measured_speed / C0), 3) 1.002
- physicskit.fields.fdtd_2d_tmz(Ez0, Hx0, Hy0, eps_r, mu_r, steps, dt, dx, dy)[source]#
Evolve a 2D TMz-mode field (Ez, Hx, Hy) on a Yee grid using leapfrog FDTD.
- Parameters:
Ez0 (
ndarray) – Initial (out-of-plane) electric field.Hx0 (
ndarray) – Initial in-plane magnetic field components.Hy0 (
ndarray) – Initial in-plane magnetic field components.eps_r (
ndarray) – Relative permittivity and permeability maps.mu_r (
ndarray) – Relative permittivity and permeability maps.steps (
int) – Number of time steps to advance.dt (
float) – Time step in seconds; must satisfycourant_limit_2d().dx (
float) – Spatial grid spacing in meters along each axis.dy (
float) – Spatial grid spacing in meters along each axis.
- Return type:
- Returns:
Ez, Hx, Hy (ndarray, shape (Nx, Ny)) – Fields at the final time step.
See also
fdtd_1dThe 1D analog of this solver.
poynting_vector_tmzEnergy flux from the evolved fields.
Examples
>>> import numpy as np >>> Nx, Ny = 50, 50 >>> Ez0 = np.zeros((Nx, Ny)) >>> Hx0 = np.zeros((Nx, Ny)) >>> Hy0 = np.zeros((Nx, Ny)) >>> eps_r, mu_r = np.ones((Nx, Ny)), np.ones((Nx, Ny)) >>> dx = dy = 1e-3 >>> dt = 0.5 * courant_limit_2d(dx, dy) >>> Ez0[Nx // 2, Ny // 2] = 1.0 >>> Ez, Hx, Hy = fdtd_2d_tmz(Ez0, Hx0, Hy0, eps_r, mu_r, steps=10, dt=dt, dx=dx, dy=dy) >>> Ez.shape (50, 50)
- physicskit.fields.fdtd_2d_tmz_evolve(Ez0, Hx0, Hy0, eps_r, mu_r, steps, dt, dx, dy, sigma=None, source=None, snapshot_stride=1)[source]#
Time-step the 2D TMz FDTD update while recording
Ezsnapshots, for animation.Unlike
fdtd_2d_tmz(), which returns only the field at the final step, this driver recordsEzeverysnapshot_stridesteps and supports two features layered on top of the same base update: a lossy absorbing boundary (sigma, e.g. frompml_conductivity_profile_2d(), so an outgoing wave is damped instead of reflecting off the hard PEC walls) and a soft source callback injected intoEzafter every field update (e.g.oscillating_dipole_source()).- Parameters:
Ez0 (
ndarray) – As infdtd_2d_tmz().Hx0 (
ndarray) – As infdtd_2d_tmz().Hy0 (
ndarray) – As infdtd_2d_tmz().eps_r (
ndarray) – As infdtd_2d_tmz().mu_r (
ndarray) – As infdtd_2d_tmz().steps (
int) – As infdtd_2d_tmz().dt (
float) – As infdtd_2d_tmz().dx (
float) – As infdtd_2d_tmz().dy (
float) – As infdtd_2d_tmz().sigma (
ndarray|None) – Electric conductivity map.None(default) reproducesfdtd_2d_tmz()’s lossless, hard-wall behavior exactly.source (callable, optional) –
source(Ez, t), called after each field update and expected to mutateEzin place (a soft source: added to, not overwriting, the field already there).snapshot_stride (
int) – RecordEzevery this many steps (plus the initial condition).
- Return type:
- Returns:
snapshots (ndarray, shape (n_recorded, Nx, Ny)) –
Ezatt=0and after every recorded step.times (ndarray, shape (n_recorded,)) – Time of each recorded snapshot.
See also
fdtd_2d_tmzThe single-shot (final-state-only) solver this wraps.
Examples
>>> import numpy as np >>> Nx, Ny = 40, 40 >>> Ez0 = np.zeros((Nx, Ny)) >>> Hx0 = Hy0 = np.zeros((Nx, Ny)) >>> eps_r = mu_r = np.ones((Nx, Ny)) >>> dx = dy = 1e-3 >>> dt = 0.5 * courant_limit_2d(dx, dy) >>> source = oscillating_dipole_source(Nx // 2, Ny // 2, amplitude=1.0, freq=5e10) >>> snaps, times = fdtd_2d_tmz_evolve(Ez0, Hx0, Hy0, eps_r, mu_r, steps=20, dt=dt, dx=dx, dy=dy, source=source, snapshot_stride=5) >>> snaps.shape (5, 40, 40)
- physicskit.fields.flux_tube_energy_density_2d(shape, dx, dy, separation, flux_quantum=1.0, tube_width=None)[source]#
2D energy-density map of the confined flux tube, for visualizing it as a stretching heatmap.
Extrudes
flux_tube_field_1d()(evaluated along the axis joining the two charges) into 2D by giving it a fixed-width Gaussian transverse profile – the “fixed cross-section” that produces confinement – and forms the field energy density \(\tfrac{1}{2}\,\text{field}(x)^2\times\text{transverse}(y)^2\).- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.dx (
float) – Grid spacing; the charges are placed on the centered grid at \(x=\pm\text{separation}/2\), \(y=0\).dy (
float) – Grid spacing; the charges are placed on the centered grid at \(x=\pm\text{separation}/2\), \(y=0\).separation (
float) – Distance between the two charges.flux_quantum (
float) – Charge magnitude, as influx_tube_field_1d().tube_width (
float|None) – Transverse (y) width of the flux tube. Defaults to5 * dy.
- Return type:
- Returns:
ndarray, shape (Nx, Ny) – Energy density, largest along the tube connecting the charges and decaying away from its axis.
Examples
>>> e = flux_tube_energy_density_2d((200, 40), dx=0.2, dy=0.2, separation=10.0) >>> bool(e.sum() > 0) True
- physicskit.fields.flux_tube_field_1d(x, separation, flux_quantum=1.0, wall_width=None)[source]#
Static 1D confined flux profile between two opposite point charges (toy confinement model).
Solves the 1D Gauss law \(d(\text{flux})/dx = \rho(x)\) for a field confined to a fixed-cross-section tube connecting two opposite charges at \(x=\pm\text{separation}/2\), each smoothed into a Gaussian of width
wall_width(a numerical regularization of the point charge, not a physical effect). The field is computed by direct numerical integration (cumulative trapezoidal sum) of this charge density, so it is genuinely “solved for” at each separation rather than assumed.- Parameters:
x (
ndarray) – 1D grid the charges live on.separation (
float) – Distance between the two charges.flux_quantum (
float) – Charge magnitude (equivalently, the plateau field strength inside the tube).wall_width (
float|None) – Smoothing width of each point charge. Defaults to3 * (x[1] - x[0]), i.e. a few grid cells – small enough to look like a point charge but resolved on the grid.
- Return type:
- Returns:
ndarray, same shape as
x– The confined field, approximately+flux_quantumbetween the charges and0outside, with smooth transitions of widthwall_width.
See also
flux_tube_energy_density_2dThe corresponding 2D energy-density map, for animation.
Examples
>>> import numpy as np >>> x = np.linspace(-20, 20, 2000) >>> field = flux_tube_field_1d(x, separation=10.0, flux_quantum=1.0) >>> bool(abs(field[np.argmin(np.abs(x))] - 1.0) < 0.05) True >>> bool(abs(field[0]) < 0.05) True
- physicskit.fields.gpe_energy(psi, V, g, X, Y, K2)[source]#
Evaluate the energy and angular momentum of a GPE wavefunction.
- Parameters:
psi (
ndarray) – Wavefunction.V (
ndarray) – External trapping potential.g (
float) – Interaction strength.X (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).Y (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).K2 (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).
- Return type:
- Returns:
dict –
{"kinetic", "potential", "interaction", "angular_momentum", "total"}, wheretotalis the lab-frame energy (kinetic + potential + interaction) andangular_momentumis \(\langle L_z \rangle\). The rotating-frame energy at rotation rateOmegaistotal - Omega * angular_momentum.
Examples
>>> import numpy as np >>> X, Y, KX, KY, K2 = harmonic_trap_grid(48, 10.0) >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi *= 1.0 / np.sqrt(np.sum(np.abs(psi) ** 2) * (X[1, 0] - X[0, 0]) ** 2) >>> E = gpe_energy(psi, V, g=0.0, X=X, Y=Y, K2=K2) >>> round(float(E["kinetic"] + E["potential"]), 4) 1.0
- physicskit.fields.gpe_evolve(psi0, V, g, dt, steps, K2, snapshot_stride=1)[source]#
Real-time propagation of the 2D Gross-Pitaevskii / cubic NLS equation via split-step Fourier.
Solves \(i\partial_t\psi = [-\tfrac{1}{2}\nabla^2 + V + g|\psi|^2]\psi\) forward in real time (unlike
gpe_relax()’s imaginary-time relaxation, which only finds stationary states): the kinetic term is exact in Fourier space, and the potential-plus-nonlinear term is exact as a pointwise phase rotation in real space, the same split-step structure asphysicskit.fields.solitons.nls_evolve()generalized to 2D and to an external potential. Real-time evolution is unitary and conserves the norm on its own (no renormalization needed, unlike imaginary time).This one stepper serves two purposes, distinguished only by
Vand the sign ofg:With a harmonic trap (
V = 0.5*(X**2+Y**2)) and repulsive interactions (g > 0), an off-center vortex genuinely precesses around the trap under the density gradient – real vortex dynamics, as opposed to the static relaxed stategpe_relax()finds.With
V = 0(or weak) and attractive interactions (g < 0here, opposite sign convention fromnls_evolve’sgsince this module’s equation carries a+g|psi|^2term), a sufficiently tall, narrow initial packet self-focuses, concentrating into a narrower, taller peak – a numerical stand-in for wave collapse. True collapse is a singularity in finite time; this integrator (like any finite-grid scheme) cannot resolve it and the calculation should be stopped once the peak density is still visibly growing, not carried through the blow-up itself.
- Parameters:
psi0 (
ndarray) – Initial wavefunction.V (
ndarray) – External potential (usenp.zeros_likefor the free/collapse case).g (
float) – Interaction strength and sign (see above).dt (
float) – Time step.steps (
int) – Number of steps to advance.K2 (
ndarray) – Wavenumber-squared grid fromharmonic_trap_grid().snapshot_stride (
int) – Record a snapshot every this many steps (plus the initial condition).
- Return type:
- Returns:
snapshots (ndarray of complex, shape (n_recorded, n, n)) – Wavefunction at
t=0and after every recorded step.times (ndarray, shape (n_recorded,)) – Time of each recorded snapshot.
See also
gpe_relaxImaginary-time relaxation to a stationary state.
physicskit.fields.solitons.nls_evolveThe 1D analog (opposite sign convention for
g).
Examples
Norm is conserved under real-time evolution, unlike the imaginary-time propagation in
gpe_relax()(which requires explicit renormalization):>>> import numpy as np >>> n, length = 48, 12.0 >>> X, Y, KX, KY, K2 = harmonic_trap_grid(n, length) >>> dxg = X[1, 0] - X[0, 0] >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi0 *= 1.0 / np.sqrt(np.sum(np.abs(psi0) ** 2) * dxg * dxg) >>> snaps, times = gpe_evolve(psi0, V, g=2.0, dt=1e-3, steps=200, K2=K2, snapshot_stride=50) >>> norms = np.sum(np.abs(snaps) ** 2, axis=(1, 2)) * dxg * dxg >>> bool(np.max(np.abs(norms - norms[0])) < 1e-6) True
- physicskit.fields.gpe_imprint_vortex(psi, X, Y, positions)[source]#
Imprint one singly-quantized vortex per position by multiplying in a phase winding.
- Parameters:
psi (
ndarray) – Wavefunction to imprint onto.X (
ndarray) – Real-space coordinate grids fromharmonic_trap_grid().Y (
ndarray) – Real-space coordinate grids fromharmonic_trap_grid().positions (sequence of tuple(float, float)) –
(x0, y0)center of each vortex to imprint.
- Return type:
- Returns:
ndarray of complex, shape (n, n) –
psimultiplied by \(\prod_i [(x-x_{0,i}) + i(y-y_{0,i})]\), renormalized to preserve the input norm.
Examples
>>> import numpy as np >>> X, Y, KX, KY, K2 = harmonic_trap_grid(32, 12.0) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi = gpe_imprint_vortex(psi0, X, Y, [(0.0, 0.0)]) >>> bool(abs(psi[16, 16]) < 1e-10) True
- physicskit.fields.gpe_relax(psi0, V, g, dtau, steps, X, Y, K2, Omega=0.0, n_particles=1.0)[source]#
Relax a GPE initial condition toward a stationary state via imaginary-time propagation.
- Parameters:
psi0 (
ndarray) – Initial wavefunction.V (
ndarray) – External trapping potential.g (
float) – Interaction (nonlinearity) strength.dtau (
float) – Imaginary-time step.steps (
int) – Number of steps to advance.X (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).Y (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).K2 (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).Omega (
float) – Rotation frequency of the trap.n_particles (
float) – Total particle number; the wavefunction is renormalized to this value after every step (imaginary-time propagation does not conserve norm on its own).
- Return type:
- Returns:
ndarray of complex, shape (n, n) – The relaxed (approximately stationary) wavefunction.
See also
gpe_energyEvaluate the energy of a relaxed state.
gpe_imprint_vortexSeed an initial condition with quantized vortices.
Notes
Energy decreases monotonically under this propagation whenever it is implemented correctly (a basic sanity check worth verifying on any new potential or parameter regime).
Examples
Comparing the rotating-frame energy of the vortex-free ground state against a state seeded with one centered vortex reproduces the standard vortex-nucleation criterion: \(\Omega_c = \Delta E/\Delta L_z\), above which the vortex state has lower energy in the rotating frame:
>>> import numpy as np >>> n, length, g = 64, 12.0, 4.0 >>> X, Y, KX, KY, K2 = harmonic_trap_grid(n, length) >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi_vf = gpe_relax(np.exp(-0.5 * (X**2 + Y**2)).astype(complex), V, g, dtau=5e-4, steps=4000, X=X, Y=Y, K2=K2) >>> psi0_v = gpe_imprint_vortex(np.exp(-0.5 * (X**2 + Y**2)).astype(complex), X, Y, [(0.0, 0.0)]) >>> psi_v = gpe_relax(psi0_v, V, g, dtau=5e-4, steps=4000, X=X, Y=Y, K2=K2) >>> E0, E1 = gpe_energy(psi_vf, V, g, X, Y, K2), gpe_energy(psi_v, V, g, X, Y, K2) >>> Omega_c = (E1["total"] - E0["total"]) / (E1["angular_momentum"] - E0["angular_momentum"]) >>> bool(0.0 < Omega_c < 1.0) True
- physicskit.fields.harmonic_trap_grid(n, length)[source]#
Build a centered real-space and wavenumber grid for a 2D harmonic trap.
- Parameters:
- Return type:
- Returns:
X, Y (ndarray, shape (n, n)) – Real-space coordinate grids, centered at the trap origin.
KX, KY (ndarray, shape (n, n)) – Wavenumber grids.
K2 (ndarray, shape (n, n)) – \(K_X^2 + K_Y^2\).
Examples
>>> X, Y, KX, KY, K2 = harmonic_trap_grid(32, 12.0) >>> float(X[0, 0]) -6.0
- physicskit.fields.kdv_evolve(u0, x, dt, steps)[source]#
Evolve a KdV initial condition forward in time on a periodic domain.
- Parameters:
- Return type:
- Returns:
ndarray – Field after
steps * dttime units.
Notes
Because KdV solitons are true solitons, two colliding solitons of different speed pass through each other elastically: each emerges from the collision with its original amplitude and speed, shifted only in phase – unlike generic nonlinear waves, which would break up or change shape on collision.
Examples
A fast, tall soliton launched behind a slower, shorter one overtakes it; both retain their original amplitudes after the collision:
>>> import numpy as np >>> from scipy.signal import find_peaks >>> N, L = 512, 60.0 >>> x = np.linspace(-L / 2, L / 2, N, endpoint=False) >>> u0 = kdv_soliton(x, c=9.0, x0=-20) + kdv_soliton(x, c=4.0, x0=-8) >>> u = kdv_evolve(u0, x, dt=0.0005, steps=6000) >>> peaks, _ = find_peaks(u, height=1.0) >>> heights = sorted(u[peaks]) >>> [round(float(h), 1) for h in heights] [2.0, 4.5]
- physicskit.fields.kdv_evolve_frames(u0, x, dt, steps_per_frame, n_frames)[source]#
Evolve a KdV field and record a snapshot every
steps_per_framesteps, for animation.A thin looping wrapper around
kdv_evolve()– no new physics, just repeated short calls with each chunk’s output fed back in as the next chunk’s initial condition (valid sincekdv_evolve()is a deterministic one-step-at-a-time integrator), so the trajectory can be animated frame by frame.- Parameters:
- Return type:
- Returns:
frames (ndarray, shape (n_frames + 1, len(x))) – Field at
t=0and after each recorded chunk.times (ndarray, shape (n_frames + 1,))
See also
physicskit.fields.visualizers.animate_field_1dRender these frames as a line-plot animation.
Examples
>>> import numpy as np >>> x = np.linspace(-30, 30, 256, endpoint=False) >>> u0 = kdv_soliton(x, c=4.0, x0=-15) >>> frames, times = kdv_evolve_frames(u0, x, dt=0.001, steps_per_frame=200, n_frames=5) >>> frames.shape (6, 256)
- physicskit.fields.kdv_soliton(x, c, x0=0.0)[source]#
Exact single-soliton solution of \(u_t + 6uu_x + u_{xxx} = 0\) at \(t=0\).
- Parameters:
- Return type:
- Returns:
ndarray – \(u(x, 0) = \tfrac{c}{2}\,\mathrm{sech}^2\!\big(\tfrac{\sqrt{c}}{2}(x-x_0)\big)\).
See also
kdv_evolvePropagate this (or any) initial condition forward in time.
Examples
>>> import numpy as np >>> round(float(kdv_soliton(0.0, c=4.0)), 6) 2.0
- physicskit.fields.kdv_step(u_hat, k, dt)[source]#
Advance the KdV equation in Fourier space by one Strang-split step.
- physicskit.fields.nls_bright_soliton(x, t, A=1.0, v=0.0, x0=0.0)[source]#
Exact bright-soliton solution of the focusing NLS equation.
Solves \(i\psi_t + \tfrac{1}{2}\psi_{xx} + |\psi|^2\psi = 0\).
- Parameters:
- Return type:
- Returns:
ndarray of complex – \(\psi(x,t) = A\,\mathrm{sech}(A(x-x_0-vt))\, e^{i[v(x-x_0) + (A^2-v^2)t/2]}\).
See also
nls_dark_solitonThe defocusing-equation counterpart.
nls_evolvePropagate this (or any) initial condition numerically.
Examples
>>> import numpy as np >>> x = np.array([0.0]) >>> abs(nls_bright_soliton(x, t=0.0, A=1.0)) array([1.])
- physicskit.fields.nls_dark_soliton(x, t, rho0=1.0, x0=0.0)[source]#
Exact stationary dark (black) soliton solution of the defocusing NLS equation.
Solves \(i\psi_t + \tfrac{1}{2}\psi_{xx} - |\psi|^2\psi = 0\): a density notch that vanishes at its center, sitting on a uniform background of density
rho0.- Parameters:
- Return type:
- Returns:
ndarray of complex – \(\psi(x,t) = \sqrt{\rho_0}\,\tanh\!\big(\sqrt{\rho_0}\,(x-x_0)\big)\, e^{-i\rho_0 t}\), with healing length \(\xi = 1/\sqrt{\rho_0}\).
See also
nls_bright_solitonThe focusing-equation counterpart.
Examples
>>> import numpy as np >>> x = np.array([0.0]) >>> abs(nls_dark_soliton(x, t=0.0, rho0=1.0)) array([0.])
- physicskit.fields.nls_evolve(psi0, x, dt, steps, g=1.0)[source]#
Evolve a nonlinear Schrodinger initial condition via split-step Fourier.
Solves \(i\psi_t + \tfrac{1}{2}\psi_{xx} + g|\psi|^2\psi = 0\) on a periodic domain:
g > 0is focusing (bright solitons),g < 0is defocusing (dark solitons).- Parameters:
- Return type:
- Returns:
ndarray of complex – Field after
steps * dttime units.
Examples
A bright soliton propagates without dispersing – its envelope \(|\psi|\) is unchanged after 2 time units of evolution:
>>> import numpy as np >>> N, L = 1024, 80.0 >>> x = np.linspace(-L / 2, L / 2, N, endpoint=False) >>> psi0 = nls_bright_soliton(x, t=0.0, A=1.0) >>> psi = nls_evolve(psi0, x, dt=0.001, steps=2000, g=1.0) >>> shape_error = np.max(np.abs(np.abs(psi) - np.abs(psi0))) >>> bool(shape_error < 1e-3) True
- physicskit.fields.nls_evolve_frames(psi0, x, dt, steps_per_frame, n_frames, g=1.0)[source]#
Evolve an NLS field and record a snapshot every
steps_per_framesteps, for animation.A thin looping wrapper around
nls_evolve(), analogous tokdv_evolve_frames(): no new physics, just repeated chunked calls with the state carried forward, to produce an animatable sequence of frames.- Parameters:
psi0 (
ndarray) – Initial wavefunction.x (
ndarray) – Uniformly spaced periodic spatial grid.dt (
float) – Time step.steps_per_frame (
int) – Number of integration steps between recorded frames.n_frames (
int) – Number of frames to record after the initial condition.g (
float) – Nonlinearity strength and sign.
- Return type:
- Returns:
frames (ndarray of complex, shape (n_frames + 1, len(x))) – Field at
t=0and after each recorded chunk.times (ndarray, shape (n_frames + 1,))
See also
physicskit.fields.visualizers.animate_field_1dRender these frames as a line-plot animation.
Examples
>>> import numpy as np >>> x = np.linspace(-40, 40, 512, endpoint=False) >>> psi0 = nls_bright_soliton(x, t=0.0, A=1.0) >>> frames, times = nls_evolve_frames(psi0, x, dt=0.001, steps_per_frame=200, n_frames=5, g=1.0) >>> frames.shape (6, 512)
- physicskit.fields.oscillating_dipole_source(i0, j0, amplitude, freq)[source]#
Build a soft, sinusoidally-oscillating point source for
fdtd_2d_tmz_evolve().Models a simple oscillating dipole antenna: a source current concentrated at a single Yee cell, injected as a soft source (added to the existing field rather than overwriting it, so outgoing waves already at that cell are not clobbered).
- Parameters:
- Returns:
callable –
source(Ez, t), suitable forfdtd_2d_tmz_evolve()’ssourceargument: addsamplitude * sin(2*pi*freq*t)toEz[i0, j0]in place.
Examples
>>> import numpy as np >>> Ez = np.zeros((10, 10)) >>> source = oscillating_dipole_source(5, 5, amplitude=2.0, freq=1.0) >>> source(Ez, t=0.25) >>> round(float(Ez[5, 5]), 6) 2.0
- physicskit.fields.plot_bec_density(X, Y, psi, ax=None)[source]#
Plot the condensate density \(|\psi(\mathbf{r})|^2\) as a heatmap.
- Parameters:
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
See also
plot_bec_phasePlot the phase, which reveals vortex cores as singularities.
Examples
>>> import numpy as np >>> from physicskit.fields.quantum_fields import harmonic_trap_grid >>> X, Y, KX, KY, K2 = harmonic_trap_grid(40, 10.0) >>> psi = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> fig, ax = plot_bec_density(X, Y, psi) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.plot_bec_phase(X, Y, psi, ax=None)[source]#
Plot the condensate phase \(\arg\psi(\mathbf{r})\); vortex cores appear as \(2\pi\) singularities.
- Parameters:
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
See also
plot_bec_densityPlot the density, which shows vortex cores as zeros.
physicskit.fields.quantum_fields.count_vorticesQuantitative vortex detection.
Examples
>>> import numpy as np >>> from physicskit.fields.quantum_fields import harmonic_trap_grid, gpe_imprint_vortex >>> X, Y, KX, KY, K2 = harmonic_trap_grid(40, 10.0) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi = gpe_imprint_vortex(psi0, X, Y, [(0.0, 0.0)]) >>> fig, ax = plot_bec_phase(X, Y, psi) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.plot_field_1d(x, u, ax=None, label=None)[source]#
Plot a 1D field snapshot (a KdV, NLS envelope, or Sine-Gordon profile).
- Parameters:
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
Examples
>>> import numpy as np >>> from physicskit.fields.solitons import kdv_soliton >>> x = np.linspace(-20, 20, 200) >>> fig, ax = plot_field_1d(x, kdv_soliton(x, c=4.0)) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.plot_poynting_field(X, Y, Sx, Sy, ax=None, stride=4)[source]#
Quiver-plot the Poynting energy-flux field over a 2D grid.
- Parameters:
X (
ndarray) – Real-space coordinate grids.Y (
ndarray) – Real-space coordinate grids.Sx (
ndarray) – Poynting vector components, as fromphysicskit.fields.electrodynamics.poynting_vector_tmz().Sy (
ndarray) – Poynting vector components, as fromphysicskit.fields.electrodynamics.poynting_vector_tmz().ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
stride (
int) – Subsample everystride-th grid point, to keep the quiver plot legible.
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
Examples
>>> import numpy as np >>> X, Y = np.meshgrid(np.linspace(-1, 1, 20), np.linspace(-1, 1, 20)) >>> Sx, Sy = -Y, X >>> fig, ax = plot_poynting_field(X, Y, Sx, Sy) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.pml_conductivity_profile(n_cells, pml_width, dx, order=3, sigma_max=None)[source]#
Build a polynomial-graded electric conductivity profile absorbing at both ends of a 1D grid.
A simplified (non-split-field) approximation to Berenger’s Perfectly Matched Layer: rather than solving auxiliary split-field equations, this grades the ordinary medium’s electric conductivity smoothly up from zero over
pml_widthcells at each boundary, damping outgoing waves via the standard lossy-dielectric FDTD update (seesigmainfdtd_1d()) instead of reflecting them off a hard wall.- Parameters:
n_cells (
int) – Total number of grid points.pml_width (
int) – Number of cells over which the conductivity ramps from 0 tosigma_max.dx (
float) – Spatial grid spacing in meters, used to scale the defaultsigma_max.order (
int) – Polynomial grading order (Sadiku’s rule of thumb uses a cubic ramp).sigma_max (
float|None) – Peak conductivity at the outermost cell. Defaults to the standard empirical optimum(order + 1) / (150 * pi * dx).
- Return type:
- Returns:
ndarray, shape (n_cells,) – Conductivity profile, zero in the interior and ramping up at both edges.
Examples
>>> sigma = pml_conductivity_profile(200, pml_width=20, dx=1e-3) >>> bool(np.all(sigma[20:-20] == 0)) True >>> bool(sigma[0] > sigma[10] > 0) True
- physicskit.fields.pml_conductivity_profile_2d(shape, pml_width, dx, dy, order=3, sigma_max=None)[source]#
Build a 2D graded-conductivity absorbing boundary by combining two 1D PML profiles.
Reuses
pml_conductivity_profile()along each axis and takes the pointwise maximum, so a cell absorbs at whichever rate is larger – correct in the corners, where both thexandyramps are active.- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.pml_width (
int) – Number of cells over which each axis’s conductivity ramps up.dx (
float) – Spatial grid spacing along each axis.dy (
float) – Spatial grid spacing along each axis.order (
int) – Polynomial grading order, passed topml_conductivity_profile().sigma_max (
float|None) – Peak conductivity; defaults (per axis) to the standard empirical optimum.
- Return type:
- Returns:
ndarray, shape (Nx, Ny) – Conductivity map, zero in the interior and ramping up at all four edges.
Examples
>>> sigma = pml_conductivity_profile_2d((100, 80), pml_width=10, dx=1e-3, dy=1e-3) >>> bool(np.all(sigma[10:-10, 10:-10] == 0)) True >>> bool(sigma[0, 0] > 0) True
- physicskit.fields.poynting_vector_tmz(Ez, Hx, Hy)[source]#
Poynting vector \(\mathbf{S} = \mathbf{E} \times \mathbf{H}\) for a TMz-mode field.
- Parameters:
- Return type:
- Returns:
Sx, Sy (ndarray) – In-plane energy flux density components, \(S_x = -E_z H_y\), \(S_y = E_z H_x\).
Examples
>>> import numpy as np >>> Ez = np.array([[2.0]]) >>> Hx = np.array([[3.0]]) >>> Hy = np.array([[5.0]]) >>> poynting_vector_tmz(Ez, Hx, Hy) (array([[-10.]]), array([[6.]]))
- physicskit.fields.sine_gordon_evolve(u0, u0_prev, x, dt, steps)[source]#
Evolve the Sine-Gordon equation with explicit leapfrog finite differences.
- Parameters:
u0 (
ndarray) – Field at the starting time.u0_prev (
ndarray) – Field one step before the starting time (needed to seed the two-level leapfrog scheme); for a traveling-wave initial condition, use the exact solution evaluated att=-dt.x (
ndarray) – Uniformly spaced spatial grid, with fixed (Dirichlet) boundaries.dt (
float) – Time step; must satisfy the CFL conditiondt <= dx(wave speed 1).steps (
int) – Number of steps to advance.
- Return type:
- Returns:
u, u_prev (ndarray) – The field at the final step and the step before it (the pair needed to continue the integration further).
Examples
A kink launched at speed \(v=0.5\) arrives at the expected Lorentz-contracted position after propagating:
>>> import numpy as np >>> N, L, v, x0 = 4000, 200.0, 0.5, -50.0 >>> x = np.linspace(-L / 2, L / 2, N) >>> dx = x[1] - x[0] >>> dt = 0.4 * dx >>> u_prev = sine_gordon_kink(x, -dt, v, x0) >>> u0 = sine_gordon_kink(x, 0.0, v, x0) >>> steps = 1000 >>> u, _ = sine_gordon_evolve(u0, u_prev, x, dt, steps) >>> expected = sine_gordon_kink(x, steps * dt, v, x0) >>> bool(np.max(np.abs(u - expected)) < 0.01) True
- physicskit.fields.sine_gordon_evolve_frames(u0, u0_prev, x, dt, steps_per_frame, n_frames)[source]#
Evolve a Sine-Gordon field and record a snapshot every
steps_per_framesteps, for animation.A thin looping wrapper around
sine_gordon_evolve(), analogous tokdv_evolve_frames(): the(u, u_prev)leapfrog pair is carried forward chunk to chunk, with no change to the underlying physics.- Parameters:
u0 (
ndarray) – Field at the starting time.u0_prev (
ndarray) – Field one step before the starting time (seesine_gordon_evolve()).x (
ndarray) – Uniformly spaced spatial grid.dt (
float) – Time step.steps_per_frame (
int) – Number of integration steps between recorded frames.n_frames (
int) – Number of frames to record after the initial condition.
- Return type:
- Returns:
frames (ndarray, shape (n_frames + 1, len(x))) – Field at
t=0and after each recorded chunk.times (ndarray, shape (n_frames + 1,))
See also
physicskit.fields.visualizers.animate_field_1dRender these frames as a line-plot animation.
Examples
>>> import numpy as np >>> x = np.linspace(-50, 50, 800) >>> dt = 0.4 * (x[1] - x[0]) >>> u_prev = sine_gordon_kink(x, -dt, v=0.5, x0=-20) >>> u0 = sine_gordon_kink(x, 0.0, v=0.5, x0=-20) >>> frames, times = sine_gordon_evolve_frames(u0, u_prev, x, dt, steps_per_frame=100, n_frames=4) >>> frames.shape (5, 800)
- physicskit.fields.sine_gordon_kink(x, t, v=0.0, x0=0.0, polarity=1)[source]#
Exact kink (or antikink) solution of the Sine-Gordon equation \(u_{tt} - u_{xx} + \sin u = 0\).
- Parameters:
- Return type:
- Returns:
ndarray – \(u(x,t) = 4\arctan\!\big(\exp[\text{polarity}\cdot\gamma(x-x_0-vt)]\big)\), with \(\gamma = 1/\sqrt{1-v^2}\).
See also
sine_gordon_evolvePropagate this (or any) initial condition numerically.
Examples
>>> import numpy as np >>> round(float(sine_gordon_kink(0.0, t=0.0)), 6) 3.141593
- physicskit.fields.tmz_cavity_mode(shape, dx, dy, m, n)[source]#
The analytic \(TM_{mn}\) standing-wave mode of an ideal rectangular PEC cavity.
fdtd_2d_tmz()(andfdtd_2d_tmz_evolve()withoutsigma) already enforceEz=0on all four edges every step – exactly a perfectly-conducting (PEC) cavity wall, no absorbing boundary needed. Seeding the grid with this analytic mode shape (which already vanishes on the boundary) launches a standing wave that should oscillate in place atomega_mnrather than propagate.- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.dx (
float) – Grid spacing; the cavity spansLx=(Nx-1)*dxbyLy=(Ny-1)*dy.dy (
float) – Grid spacing; the cavity spansLx=(Nx-1)*dxbyLy=(Ny-1)*dy.m (
int) – Mode indices (number of half-wavelengths along each axis).n (
int) – Mode indices (number of half-wavelengths along each axis).
- Return type:
- Returns:
Ez0 (ndarray, shape (Nx, Ny)) – \(\sin(m\pi x/L_x)\sin(n\pi y/L_y)\).
omega_mn (float) – The mode’s angular frequency, \(c_0\pi\sqrt{(m/L_x)^2+(n/L_y)^2}\).
Examples
>>> Ez0, omega = tmz_cavity_mode((41, 41), dx=1e-3, dy=1e-3, m=1, n=1) >>> bool(np.all(Ez0[0, :] == 0) and np.all(Ez0[:, 0] == 0)) True >>> bool(omega > 0) True
Finite-difference time-domain (FDTD) solutions of Maxwell’s equations on a Yee grid.
Implements the staggered-grid (Yee 1966) leapfrog update for 1D plane waves and 2D TMz-mode propagation, plus a graded-conductivity absorbing boundary (a simplified, non-split-field approximation to Berenger’s Perfectly Matched Layer) that damps outgoing waves instead of reflecting them off a hard electric wall.
Units are SI throughout; EPS0, MU0, C0 are the
vacuum permittivity, permeability, and speed of light.
- physicskit.fields.electrodynamics.courant_limit_1d(dx)[source]#
Maximum stable time step for the 1D FDTD update.
- Parameters:
dx (
float) – Spatial grid spacing in meters.- Return type:
- Returns:
float – The Courant stability limit \(\Delta t = \Delta x / c_0\).
Examples
>>> round(float(courant_limit_1d(1e-3) * C0 / 1e-3), 6) 1.0
- physicskit.fields.electrodynamics.courant_limit_2d(dx, dy)[source]#
Maximum stable time step for the 2D FDTD update.
- Parameters:
- Return type:
- Returns:
float – The Courant stability limit \(\Delta t = 1 / (c_0 \sqrt{1/\Delta x^2 + 1/\Delta y^2})\).
Examples
>>> dt = courant_limit_2d(1e-3, 1e-3) >>> round(float(dt * C0 / 1e-3), 6) 0.707107
- physicskit.fields.electrodynamics.dielectric_slab(shape, i_start, i_end, eps_r_slab=4.0)[source]#
A planar dielectric slab:
eps_r_slabbetween grid columnsi_startandi_end, vacuum elsewhere.A minimal demo permittivity map for
fdtd_2d_tmz()/fdtd_2d_tmz_evolve(): a wave launched from one side crossingi_startpartially reflects and partially refracts/transmits into the slab, and again ati_endon the way out, illustrating propagation through a medium interface.- Parameters:
- Return type:
- Returns:
ndarray, shape (Nx, Ny) –
eps_rmap:eps_r_slabinside the slab,1.0outside.
Examples
>>> eps_r = dielectric_slab((40, 20), i_start=15, i_end=25, eps_r_slab=4.0) >>> float(eps_r[10, 5]), float(eps_r[20, 5]) (1.0, 4.0)
- physicskit.fields.electrodynamics.fdtd_1d(Ez0, Hy0, eps_r, mu_r, steps, dt, dx, sigma=None)[source]#
Evolve a 1D plane wave (Ez, Hy) on a Yee grid using leapfrog FDTD.
Discretizes Maxwell’s curl equations for fields varying only along
x, \(\partial_t H_y = \partial_x E_z/\mu\) and \(\partial_t E_z = \partial_x H_y/\epsilon\) (the same signs asfdtd_2d_tmz()), so a wave moving toward+xhas \(H_y = -E_z/\eta\) (Poynting vector \(-E_zH_y>0\)).- Parameters:
Ez0 (
ndarray) – Initial electric field.Hy0 (
ndarray) – Initial magnetic field, staggered half a cell ahead ofEz0.eps_r (
ndarray) – Relative permittivity and permeability at eachEzgrid point.mu_r (
ndarray) – Relative permittivity and permeability at eachEzgrid point.steps (
int) – Number of time steps to advance.dt (
float) – Time step in seconds; must satisfycourant_limit_1d().dx (
float) – Spatial grid spacing in meters.sigma (
ndarray|None) – Electric conductivity (S/m) at each grid point, e.g. frompml_conductivity_profile(), for a lossy/absorbing boundary. Defaults to a lossless grid with hard (PEC) walls at both ends.
- Return type:
- Returns:
Ez, Hy (ndarray) – Fields at the final time step, same shapes as
Ez0,Hy0.
See also
fdtd_2d_tmzThe 2D (TMz-mode) analog of this solver.
Examples
A smooth Gaussian pulse, launched with the impedance-matched initial condition of a purely right-moving wave (\(H_y=-E_z/\eta_0\)), propagates at exactly the vacuum speed of light:
>>> import numpy as np >>> N = 800 >>> dx = 1e-3 >>> dt = 0.99 * courant_limit_1d(dx) >>> eta0 = np.sqrt(MU0 / EPS0) >>> x0, sigma_pulse = 100, 25 >>> Ez0 = np.exp(-((np.arange(N) - x0) ** 2) / (2 * sigma_pulse ** 2)) >>> xh = np.arange(N - 1) + 0.5 >>> Hy0 = -np.exp(-((xh - x0) ** 2) / (2 * sigma_pulse ** 2)) / eta0 >>> eps_r, mu_r = np.ones(N), np.ones(N) >>> Ez, Hy = fdtd_1d(Ez0, Hy0, eps_r, mu_r, steps=360, dt=dt, dx=dx) >>> peak = np.argmax(Ez) >>> measured_speed = (peak - x0) * dx / (360 * dt) >>> round(float(measured_speed / C0), 3) 1.002
- physicskit.fields.electrodynamics.fdtd_2d_tmz(Ez0, Hx0, Hy0, eps_r, mu_r, steps, dt, dx, dy)[source]#
Evolve a 2D TMz-mode field (Ez, Hx, Hy) on a Yee grid using leapfrog FDTD.
- Parameters:
Ez0 (
ndarray) – Initial (out-of-plane) electric field.Hx0 (
ndarray) – Initial in-plane magnetic field components.Hy0 (
ndarray) – Initial in-plane magnetic field components.eps_r (
ndarray) – Relative permittivity and permeability maps.mu_r (
ndarray) – Relative permittivity and permeability maps.steps (
int) – Number of time steps to advance.dt (
float) – Time step in seconds; must satisfycourant_limit_2d().dx (
float) – Spatial grid spacing in meters along each axis.dy (
float) – Spatial grid spacing in meters along each axis.
- Return type:
- Returns:
Ez, Hx, Hy (ndarray, shape (Nx, Ny)) – Fields at the final time step.
See also
fdtd_1dThe 1D analog of this solver.
poynting_vector_tmzEnergy flux from the evolved fields.
Examples
>>> import numpy as np >>> Nx, Ny = 50, 50 >>> Ez0 = np.zeros((Nx, Ny)) >>> Hx0 = np.zeros((Nx, Ny)) >>> Hy0 = np.zeros((Nx, Ny)) >>> eps_r, mu_r = np.ones((Nx, Ny)), np.ones((Nx, Ny)) >>> dx = dy = 1e-3 >>> dt = 0.5 * courant_limit_2d(dx, dy) >>> Ez0[Nx // 2, Ny // 2] = 1.0 >>> Ez, Hx, Hy = fdtd_2d_tmz(Ez0, Hx0, Hy0, eps_r, mu_r, steps=10, dt=dt, dx=dx, dy=dy) >>> Ez.shape (50, 50)
- physicskit.fields.electrodynamics.fdtd_2d_tmz_evolve(Ez0, Hx0, Hy0, eps_r, mu_r, steps, dt, dx, dy, sigma=None, source=None, snapshot_stride=1)[source]#
Time-step the 2D TMz FDTD update while recording
Ezsnapshots, for animation.Unlike
fdtd_2d_tmz(), which returns only the field at the final step, this driver recordsEzeverysnapshot_stridesteps and supports two features layered on top of the same base update: a lossy absorbing boundary (sigma, e.g. frompml_conductivity_profile_2d(), so an outgoing wave is damped instead of reflecting off the hard PEC walls) and a soft source callback injected intoEzafter every field update (e.g.oscillating_dipole_source()).- Parameters:
Ez0 (
ndarray) – As infdtd_2d_tmz().Hx0 (
ndarray) – As infdtd_2d_tmz().Hy0 (
ndarray) – As infdtd_2d_tmz().eps_r (
ndarray) – As infdtd_2d_tmz().mu_r (
ndarray) – As infdtd_2d_tmz().steps (
int) – As infdtd_2d_tmz().dt (
float) – As infdtd_2d_tmz().dx (
float) – As infdtd_2d_tmz().dy (
float) – As infdtd_2d_tmz().sigma (
ndarray|None) – Electric conductivity map.None(default) reproducesfdtd_2d_tmz()’s lossless, hard-wall behavior exactly.source (callable, optional) –
source(Ez, t), called after each field update and expected to mutateEzin place (a soft source: added to, not overwriting, the field already there).snapshot_stride (
int) – RecordEzevery this many steps (plus the initial condition).
- Return type:
- Returns:
snapshots (ndarray, shape (n_recorded, Nx, Ny)) –
Ezatt=0and after every recorded step.times (ndarray, shape (n_recorded,)) – Time of each recorded snapshot.
See also
fdtd_2d_tmzThe single-shot (final-state-only) solver this wraps.
Examples
>>> import numpy as np >>> Nx, Ny = 40, 40 >>> Ez0 = np.zeros((Nx, Ny)) >>> Hx0 = Hy0 = np.zeros((Nx, Ny)) >>> eps_r = mu_r = np.ones((Nx, Ny)) >>> dx = dy = 1e-3 >>> dt = 0.5 * courant_limit_2d(dx, dy) >>> source = oscillating_dipole_source(Nx // 2, Ny // 2, amplitude=1.0, freq=5e10) >>> snaps, times = fdtd_2d_tmz_evolve(Ez0, Hx0, Hy0, eps_r, mu_r, steps=20, dt=dt, dx=dx, dy=dy, source=source, snapshot_stride=5) >>> snaps.shape (5, 40, 40)
- physicskit.fields.electrodynamics.flux_tube_energy_density_2d(shape, dx, dy, separation, flux_quantum=1.0, tube_width=None)[source]#
2D energy-density map of the confined flux tube, for visualizing it as a stretching heatmap.
Extrudes
flux_tube_field_1d()(evaluated along the axis joining the two charges) into 2D by giving it a fixed-width Gaussian transverse profile – the “fixed cross-section” that produces confinement – and forms the field energy density \(\tfrac{1}{2}\,\text{field}(x)^2\times\text{transverse}(y)^2\).- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.dx (
float) – Grid spacing; the charges are placed on the centered grid at \(x=\pm\text{separation}/2\), \(y=0\).dy (
float) – Grid spacing; the charges are placed on the centered grid at \(x=\pm\text{separation}/2\), \(y=0\).separation (
float) – Distance between the two charges.flux_quantum (
float) – Charge magnitude, as influx_tube_field_1d().tube_width (
float|None) – Transverse (y) width of the flux tube. Defaults to5 * dy.
- Return type:
- Returns:
ndarray, shape (Nx, Ny) – Energy density, largest along the tube connecting the charges and decaying away from its axis.
Examples
>>> e = flux_tube_energy_density_2d((200, 40), dx=0.2, dy=0.2, separation=10.0) >>> bool(e.sum() > 0) True
- physicskit.fields.electrodynamics.flux_tube_field_1d(x, separation, flux_quantum=1.0, wall_width=None)[source]#
Static 1D confined flux profile between two opposite point charges (toy confinement model).
Solves the 1D Gauss law \(d(\text{flux})/dx = \rho(x)\) for a field confined to a fixed-cross-section tube connecting two opposite charges at \(x=\pm\text{separation}/2\), each smoothed into a Gaussian of width
wall_width(a numerical regularization of the point charge, not a physical effect). The field is computed by direct numerical integration (cumulative trapezoidal sum) of this charge density, so it is genuinely “solved for” at each separation rather than assumed.- Parameters:
x (
ndarray) – 1D grid the charges live on.separation (
float) – Distance between the two charges.flux_quantum (
float) – Charge magnitude (equivalently, the plateau field strength inside the tube).wall_width (
float|None) – Smoothing width of each point charge. Defaults to3 * (x[1] - x[0]), i.e. a few grid cells – small enough to look like a point charge but resolved on the grid.
- Return type:
- Returns:
ndarray, same shape as
x– The confined field, approximately+flux_quantumbetween the charges and0outside, with smooth transitions of widthwall_width.
See also
flux_tube_energy_density_2dThe corresponding 2D energy-density map, for animation.
Examples
>>> import numpy as np >>> x = np.linspace(-20, 20, 2000) >>> field = flux_tube_field_1d(x, separation=10.0, flux_quantum=1.0) >>> bool(abs(field[np.argmin(np.abs(x))] - 1.0) < 0.05) True >>> bool(abs(field[0]) < 0.05) True
- physicskit.fields.electrodynamics.oscillating_dipole_source(i0, j0, amplitude, freq)[source]#
Build a soft, sinusoidally-oscillating point source for
fdtd_2d_tmz_evolve().Models a simple oscillating dipole antenna: a source current concentrated at a single Yee cell, injected as a soft source (added to the existing field rather than overwriting it, so outgoing waves already at that cell are not clobbered).
- Parameters:
- Returns:
callable –
source(Ez, t), suitable forfdtd_2d_tmz_evolve()’ssourceargument: addsamplitude * sin(2*pi*freq*t)toEz[i0, j0]in place.
Examples
>>> import numpy as np >>> Ez = np.zeros((10, 10)) >>> source = oscillating_dipole_source(5, 5, amplitude=2.0, freq=1.0) >>> source(Ez, t=0.25) >>> round(float(Ez[5, 5]), 6) 2.0
- physicskit.fields.electrodynamics.pml_conductivity_profile(n_cells, pml_width, dx, order=3, sigma_max=None)[source]#
Build a polynomial-graded electric conductivity profile absorbing at both ends of a 1D grid.
A simplified (non-split-field) approximation to Berenger’s Perfectly Matched Layer: rather than solving auxiliary split-field equations, this grades the ordinary medium’s electric conductivity smoothly up from zero over
pml_widthcells at each boundary, damping outgoing waves via the standard lossy-dielectric FDTD update (seesigmainfdtd_1d()) instead of reflecting them off a hard wall.- Parameters:
n_cells (
int) – Total number of grid points.pml_width (
int) – Number of cells over which the conductivity ramps from 0 tosigma_max.dx (
float) – Spatial grid spacing in meters, used to scale the defaultsigma_max.order (
int) – Polynomial grading order (Sadiku’s rule of thumb uses a cubic ramp).sigma_max (
float|None) – Peak conductivity at the outermost cell. Defaults to the standard empirical optimum(order + 1) / (150 * pi * dx).
- Return type:
- Returns:
ndarray, shape (n_cells,) – Conductivity profile, zero in the interior and ramping up at both edges.
Examples
>>> sigma = pml_conductivity_profile(200, pml_width=20, dx=1e-3) >>> bool(np.all(sigma[20:-20] == 0)) True >>> bool(sigma[0] > sigma[10] > 0) True
- physicskit.fields.electrodynamics.pml_conductivity_profile_2d(shape, pml_width, dx, dy, order=3, sigma_max=None)[source]#
Build a 2D graded-conductivity absorbing boundary by combining two 1D PML profiles.
Reuses
pml_conductivity_profile()along each axis and takes the pointwise maximum, so a cell absorbs at whichever rate is larger – correct in the corners, where both thexandyramps are active.- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.pml_width (
int) – Number of cells over which each axis’s conductivity ramps up.dx (
float) – Spatial grid spacing along each axis.dy (
float) – Spatial grid spacing along each axis.order (
int) – Polynomial grading order, passed topml_conductivity_profile().sigma_max (
float|None) – Peak conductivity; defaults (per axis) to the standard empirical optimum.
- Return type:
- Returns:
ndarray, shape (Nx, Ny) – Conductivity map, zero in the interior and ramping up at all four edges.
Examples
>>> sigma = pml_conductivity_profile_2d((100, 80), pml_width=10, dx=1e-3, dy=1e-3) >>> bool(np.all(sigma[10:-10, 10:-10] == 0)) True >>> bool(sigma[0, 0] > 0) True
- physicskit.fields.electrodynamics.poynting_vector_tmz(Ez, Hx, Hy)[source]#
Poynting vector \(\mathbf{S} = \mathbf{E} \times \mathbf{H}\) for a TMz-mode field.
- Parameters:
- Return type:
- Returns:
Sx, Sy (ndarray) – In-plane energy flux density components, \(S_x = -E_z H_y\), \(S_y = E_z H_x\).
Examples
>>> import numpy as np >>> Ez = np.array([[2.0]]) >>> Hx = np.array([[3.0]]) >>> Hy = np.array([[5.0]]) >>> poynting_vector_tmz(Ez, Hx, Hy) (array([[-10.]]), array([[6.]]))
- physicskit.fields.electrodynamics.tmz_cavity_mode(shape, dx, dy, m, n)[source]#
The analytic \(TM_{mn}\) standing-wave mode of an ideal rectangular PEC cavity.
fdtd_2d_tmz()(andfdtd_2d_tmz_evolve()withoutsigma) already enforceEz=0on all four edges every step – exactly a perfectly-conducting (PEC) cavity wall, no absorbing boundary needed. Seeding the grid with this analytic mode shape (which already vanishes on the boundary) launches a standing wave that should oscillate in place atomega_mnrather than propagate.- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.dx (
float) – Grid spacing; the cavity spansLx=(Nx-1)*dxbyLy=(Ny-1)*dy.dy (
float) – Grid spacing; the cavity spansLx=(Nx-1)*dxbyLy=(Ny-1)*dy.m (
int) – Mode indices (number of half-wavelengths along each axis).n (
int) – Mode indices (number of half-wavelengths along each axis).
- Return type:
- Returns:
Ez0 (ndarray, shape (Nx, Ny)) – \(\sin(m\pi x/L_x)\sin(n\pi y/L_y)\).
omega_mn (float) – The mode’s angular frequency, \(c_0\pi\sqrt{(m/L_x)^2+(n/L_y)^2}\).
Examples
>>> Ez0, omega = tmz_cavity_mode((41, 41), dx=1e-3, dy=1e-3, m=1, n=1) >>> bool(np.all(Ez0[0, :] == 0) and np.all(Ez0[:, 0] == 0)) True >>> bool(omega > 0) True
Soliton-bearing nonlinear field equations: KdV, the nonlinear Schrodinger equation, and Sine-Gordon.
Each equation is integrated with a scheme suited to its structure:
Korteweg-de Vries – Strang-split pseudo-spectral: the stiff linear dispersion \(\partial_x^3\) is advanced exactly via the FFT, and the non-stiff advection \(6u\partial_x u\) via RK4.
Nonlinear Schrodinger – split-step Fourier: exact for the linear (kinetic) part and exact for the nonlinear (pointwise phase-rotation) part.
Sine-Gordon – explicit leapfrog finite differences on the wave equation.
- physicskit.fields.solitons.kdv_evolve(u0, x, dt, steps)[source]#
Evolve a KdV initial condition forward in time on a periodic domain.
- Parameters:
- Return type:
- Returns:
ndarray – Field after
steps * dttime units.
Notes
Because KdV solitons are true solitons, two colliding solitons of different speed pass through each other elastically: each emerges from the collision with its original amplitude and speed, shifted only in phase – unlike generic nonlinear waves, which would break up or change shape on collision.
Examples
A fast, tall soliton launched behind a slower, shorter one overtakes it; both retain their original amplitudes after the collision:
>>> import numpy as np >>> from scipy.signal import find_peaks >>> N, L = 512, 60.0 >>> x = np.linspace(-L / 2, L / 2, N, endpoint=False) >>> u0 = kdv_soliton(x, c=9.0, x0=-20) + kdv_soliton(x, c=4.0, x0=-8) >>> u = kdv_evolve(u0, x, dt=0.0005, steps=6000) >>> peaks, _ = find_peaks(u, height=1.0) >>> heights = sorted(u[peaks]) >>> [round(float(h), 1) for h in heights] [2.0, 4.5]
- physicskit.fields.solitons.kdv_evolve_frames(u0, x, dt, steps_per_frame, n_frames)[source]#
Evolve a KdV field and record a snapshot every
steps_per_framesteps, for animation.A thin looping wrapper around
kdv_evolve()– no new physics, just repeated short calls with each chunk’s output fed back in as the next chunk’s initial condition (valid sincekdv_evolve()is a deterministic one-step-at-a-time integrator), so the trajectory can be animated frame by frame.- Parameters:
- Return type:
- Returns:
frames (ndarray, shape (n_frames + 1, len(x))) – Field at
t=0and after each recorded chunk.times (ndarray, shape (n_frames + 1,))
See also
physicskit.fields.visualizers.animate_field_1dRender these frames as a line-plot animation.
Examples
>>> import numpy as np >>> x = np.linspace(-30, 30, 256, endpoint=False) >>> u0 = kdv_soliton(x, c=4.0, x0=-15) >>> frames, times = kdv_evolve_frames(u0, x, dt=0.001, steps_per_frame=200, n_frames=5) >>> frames.shape (6, 256)
- physicskit.fields.solitons.kdv_soliton(x, c, x0=0.0)[source]#
Exact single-soliton solution of \(u_t + 6uu_x + u_{xxx} = 0\) at \(t=0\).
- Parameters:
- Return type:
- Returns:
ndarray – \(u(x, 0) = \tfrac{c}{2}\,\mathrm{sech}^2\!\big(\tfrac{\sqrt{c}}{2}(x-x_0)\big)\).
See also
kdv_evolvePropagate this (or any) initial condition forward in time.
Examples
>>> import numpy as np >>> round(float(kdv_soliton(0.0, c=4.0)), 6) 2.0
- physicskit.fields.solitons.kdv_step(u_hat, k, dt)[source]#
Advance the KdV equation in Fourier space by one Strang-split step.
- physicskit.fields.solitons.nls_bright_soliton(x, t, A=1.0, v=0.0, x0=0.0)[source]#
Exact bright-soliton solution of the focusing NLS equation.
Solves \(i\psi_t + \tfrac{1}{2}\psi_{xx} + |\psi|^2\psi = 0\).
- Parameters:
- Return type:
- Returns:
ndarray of complex – \(\psi(x,t) = A\,\mathrm{sech}(A(x-x_0-vt))\, e^{i[v(x-x_0) + (A^2-v^2)t/2]}\).
See also
nls_dark_solitonThe defocusing-equation counterpart.
nls_evolvePropagate this (or any) initial condition numerically.
Examples
>>> import numpy as np >>> x = np.array([0.0]) >>> abs(nls_bright_soliton(x, t=0.0, A=1.0)) array([1.])
- physicskit.fields.solitons.nls_dark_soliton(x, t, rho0=1.0, x0=0.0)[source]#
Exact stationary dark (black) soliton solution of the defocusing NLS equation.
Solves \(i\psi_t + \tfrac{1}{2}\psi_{xx} - |\psi|^2\psi = 0\): a density notch that vanishes at its center, sitting on a uniform background of density
rho0.- Parameters:
- Return type:
- Returns:
ndarray of complex – \(\psi(x,t) = \sqrt{\rho_0}\,\tanh\!\big(\sqrt{\rho_0}\,(x-x_0)\big)\, e^{-i\rho_0 t}\), with healing length \(\xi = 1/\sqrt{\rho_0}\).
See also
nls_bright_solitonThe focusing-equation counterpart.
Examples
>>> import numpy as np >>> x = np.array([0.0]) >>> abs(nls_dark_soliton(x, t=0.0, rho0=1.0)) array([0.])
- physicskit.fields.solitons.nls_evolve(psi0, x, dt, steps, g=1.0)[source]#
Evolve a nonlinear Schrodinger initial condition via split-step Fourier.
Solves \(i\psi_t + \tfrac{1}{2}\psi_{xx} + g|\psi|^2\psi = 0\) on a periodic domain:
g > 0is focusing (bright solitons),g < 0is defocusing (dark solitons).- Parameters:
- Return type:
- Returns:
ndarray of complex – Field after
steps * dttime units.
Examples
A bright soliton propagates without dispersing – its envelope \(|\psi|\) is unchanged after 2 time units of evolution:
>>> import numpy as np >>> N, L = 1024, 80.0 >>> x = np.linspace(-L / 2, L / 2, N, endpoint=False) >>> psi0 = nls_bright_soliton(x, t=0.0, A=1.0) >>> psi = nls_evolve(psi0, x, dt=0.001, steps=2000, g=1.0) >>> shape_error = np.max(np.abs(np.abs(psi) - np.abs(psi0))) >>> bool(shape_error < 1e-3) True
- physicskit.fields.solitons.nls_evolve_frames(psi0, x, dt, steps_per_frame, n_frames, g=1.0)[source]#
Evolve an NLS field and record a snapshot every
steps_per_framesteps, for animation.A thin looping wrapper around
nls_evolve(), analogous tokdv_evolve_frames(): no new physics, just repeated chunked calls with the state carried forward, to produce an animatable sequence of frames.- Parameters:
psi0 (
ndarray) – Initial wavefunction.x (
ndarray) – Uniformly spaced periodic spatial grid.dt (
float) – Time step.steps_per_frame (
int) – Number of integration steps between recorded frames.n_frames (
int) – Number of frames to record after the initial condition.g (
float) – Nonlinearity strength and sign.
- Return type:
- Returns:
frames (ndarray of complex, shape (n_frames + 1, len(x))) – Field at
t=0and after each recorded chunk.times (ndarray, shape (n_frames + 1,))
See also
physicskit.fields.visualizers.animate_field_1dRender these frames as a line-plot animation.
Examples
>>> import numpy as np >>> x = np.linspace(-40, 40, 512, endpoint=False) >>> psi0 = nls_bright_soliton(x, t=0.0, A=1.0) >>> frames, times = nls_evolve_frames(psi0, x, dt=0.001, steps_per_frame=200, n_frames=5, g=1.0) >>> frames.shape (6, 512)
- physicskit.fields.solitons.sine_gordon_evolve(u0, u0_prev, x, dt, steps)[source]#
Evolve the Sine-Gordon equation with explicit leapfrog finite differences.
- Parameters:
u0 (
ndarray) – Field at the starting time.u0_prev (
ndarray) – Field one step before the starting time (needed to seed the two-level leapfrog scheme); for a traveling-wave initial condition, use the exact solution evaluated att=-dt.x (
ndarray) – Uniformly spaced spatial grid, with fixed (Dirichlet) boundaries.dt (
float) – Time step; must satisfy the CFL conditiondt <= dx(wave speed 1).steps (
int) – Number of steps to advance.
- Return type:
- Returns:
u, u_prev (ndarray) – The field at the final step and the step before it (the pair needed to continue the integration further).
Examples
A kink launched at speed \(v=0.5\) arrives at the expected Lorentz-contracted position after propagating:
>>> import numpy as np >>> N, L, v, x0 = 4000, 200.0, 0.5, -50.0 >>> x = np.linspace(-L / 2, L / 2, N) >>> dx = x[1] - x[0] >>> dt = 0.4 * dx >>> u_prev = sine_gordon_kink(x, -dt, v, x0) >>> u0 = sine_gordon_kink(x, 0.0, v, x0) >>> steps = 1000 >>> u, _ = sine_gordon_evolve(u0, u_prev, x, dt, steps) >>> expected = sine_gordon_kink(x, steps * dt, v, x0) >>> bool(np.max(np.abs(u - expected)) < 0.01) True
- physicskit.fields.solitons.sine_gordon_evolve_frames(u0, u0_prev, x, dt, steps_per_frame, n_frames)[source]#
Evolve a Sine-Gordon field and record a snapshot every
steps_per_framesteps, for animation.A thin looping wrapper around
sine_gordon_evolve(), analogous tokdv_evolve_frames(): the(u, u_prev)leapfrog pair is carried forward chunk to chunk, with no change to the underlying physics.- Parameters:
u0 (
ndarray) – Field at the starting time.u0_prev (
ndarray) – Field one step before the starting time (seesine_gordon_evolve()).x (
ndarray) – Uniformly spaced spatial grid.dt (
float) – Time step.steps_per_frame (
int) – Number of integration steps between recorded frames.n_frames (
int) – Number of frames to record after the initial condition.
- Return type:
- Returns:
frames (ndarray, shape (n_frames + 1, len(x))) – Field at
t=0and after each recorded chunk.times (ndarray, shape (n_frames + 1,))
See also
physicskit.fields.visualizers.animate_field_1dRender these frames as a line-plot animation.
Examples
>>> import numpy as np >>> x = np.linspace(-50, 50, 800) >>> dt = 0.4 * (x[1] - x[0]) >>> u_prev = sine_gordon_kink(x, -dt, v=0.5, x0=-20) >>> u0 = sine_gordon_kink(x, 0.0, v=0.5, x0=-20) >>> frames, times = sine_gordon_evolve_frames(u0, u_prev, x, dt, steps_per_frame=100, n_frames=4) >>> frames.shape (5, 800)
- physicskit.fields.solitons.sine_gordon_kink(x, t, v=0.0, x0=0.0, polarity=1)[source]#
Exact kink (or antikink) solution of the Sine-Gordon equation \(u_{tt} - u_{xx} + \sin u = 0\).
- Parameters:
- Return type:
- Returns:
ndarray – \(u(x,t) = 4\arctan\!\big(\exp[\text{polarity}\cdot\gamma(x-x_0-vt)]\big)\), with \(\gamma = 1/\sqrt{1-v^2}\).
See also
sine_gordon_evolvePropagate this (or any) initial condition numerically.
Examples
>>> import numpy as np >>> round(float(sine_gordon_kink(0.0, t=0.0)), 6) 3.141593
Gross-Pitaevskii mean-field theory for trapped, rotating Bose-Einstein condensates.
Solves the time-dependent Gross-Pitaevskii equation (GPE)
in the rotating frame (units \(\hbar=m=1\)) by imaginary-time propagation (\(\tau=it\)), which turns the Schrodinger-like evolution into a gradient flow that relaxes any initial state toward a stationary point of the rotating-frame energy. The kinetic and potential/interaction terms are each handled exactly in their natural representation (spectral and real-space respectively, split-step); the angular-momentum term \(\Omega L_z\) is advanced explicitly via centered finite differences, which is non-stiff at the small imaginary-time steps used here.
A single quantized vortex, imprinted by multiplying the wavefunction by
\((x-x_0)+i(y-y_0)\), is used throughout as the worked example of a
“vortex lattice” building block: gpe_relax() finds it as a genuine
stationary GPE solution, and comparing its rotating-frame energy against
the vortex-free ground state reproduces the standard textbook criterion
for the critical rotation frequency \(\Omega_c\) above which nucleating
a vortex lowers the energy.
- physicskit.fields.quantum_fields.casimir_energy_1d(d, c=1.0, cutoff=None)[source]#
Regularized zero-point (Casimir) energy of the discrete 1D cavity spectrum.
Uses the standard exponential-cutoff regularization: the sum \(\sum_n n x^n = x/(1-x)^2\) (with \(x=e^{-a}\), \(a=\pi c\,\text{cutoff}/d\)) has an exact closed form, so no series truncation is needed. Its small-
cutoff(small-a) expansion is \(\tfrac{1}{4\sinh^2(a/2)} = 1/a^2 - 1/12 + O(a^2)\); the \(1/a^2\) piece is the (unphysical, cutoff-scheme-dependent) divergence that a continuum reference calculation regularized the same way would also produce, so it is subtracted exactly, leaving the finite remainder that survives ascutoff -> 0:\[E(d) \to -\frac{\pi c}{24 d}\]the standard 1D massless-field Casimir energy (equivalently, the Casimir energy of a CFT strip with central charge 1).
- Parameters:
d (
float) – Plate separation.c (
float) – Wave speed.cutoff (
float|None) – Regulator scale. Defaults to1e-3 * d / c. This subtraction is a difference of two large, nearly-equal floating-point terms (\(\propto 1/\text{cutoff}^2\)), so makingcutofftoo small loses precision to cancellation rather than gaining it;1e-4*d/cto1e-2*d/cis the well-behaved range in double precision.
- Return type:
- Returns:
float – The regularized (finite, cutoff-subtracted) zero-point energy.
See also
casimir_mode_frequenciesThe (bare, un-regularized) mode spectrum being summed.
Examples
The cutoff-and-subtract result agrees with the known closed form \(-\pi c/(24d)\), and (within the well-behaved cutoff range noted above) agrees more closely as the cutoff shrinks:
>>> d = 3.0 >>> exact = -np.pi * 1.0 / (24 * d) >>> bool(abs(casimir_energy_1d(d, cutoff=3e-3 * d) - exact) < 1e-6) True >>> bool(abs(casimir_energy_1d(d, cutoff=7e-4 * d) - exact) < 1e-8) True
The energy grows less negative (weaker confinement of vacuum energy) as the plates separate, giving an attractive force \(-dE/dd < 0\):
>>> bool(casimir_energy_1d(1.0) < casimir_energy_1d(2.0) < 0) True
- physicskit.fields.quantum_fields.casimir_mode_frequencies(d, c=1.0, n_max=200)[source]#
Discrete standing-wave mode frequencies of a 1D cavity of plate separation
d.- Parameters:
- Return type:
- Returns:
ndarray, shape (n_max,) – \(\omega_n = n\pi c/d\) for \(n=1,\dots,n_{max}\).
See also
casimir_energy_1dThe regularized zero-point energy of this mode spectrum.
Examples
>>> omega = casimir_mode_frequencies(d=2.0, c=1.0, n_max=3) >>> [round(float(w), 4) for w in omega] [1.5708, 3.1416, 4.7124]
- physicskit.fields.quantum_fields.count_vortices(psi, density_threshold=0.05)[source]#
Locate quantized vortices by summing the phase winding around each grid plaquette.
Plaquettes in very low density regions are excluded, since the phase of a near-zero-amplitude wavefunction is dominated by numerical noise and produces spurious windings there.
A vortex core that sits exactly on a grid vertex (rather than strictly inside a plaquette) can go undetected, since its circulation is then split ambiguously between the four plaquettes touching that vertex; in practice this is avoided by seeding vortices at positions not exactly on the grid.
- Parameters:
- Return type:
- Returns:
ndarray of int, shape (n, n) – Integer winding number around each plaquette’s corner
(i, j); nonzero entries mark a vortex core (+1or-1for a singly-quantized vortex/antivortex) enclosed by that plaquette.
Examples
>>> import numpy as np >>> X, Y, KX, KY, K2 = harmonic_trap_grid(48, 10.0) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi_vortex = gpe_imprint_vortex(psi0, X, Y, [(0.0, 0.0)]) >>> winding = count_vortices(psi_vortex) >>> int(np.sum(np.abs(winding))) 1
- physicskit.fields.quantum_fields.gpe_energy(psi, V, g, X, Y, K2)[source]#
Evaluate the energy and angular momentum of a GPE wavefunction.
- Parameters:
psi (
ndarray) – Wavefunction.V (
ndarray) – External trapping potential.g (
float) – Interaction strength.X (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).Y (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).K2 (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).
- Return type:
- Returns:
dict –
{"kinetic", "potential", "interaction", "angular_momentum", "total"}, wheretotalis the lab-frame energy (kinetic + potential + interaction) andangular_momentumis \(\langle L_z \rangle\). The rotating-frame energy at rotation rateOmegaistotal - Omega * angular_momentum.
Examples
>>> import numpy as np >>> X, Y, KX, KY, K2 = harmonic_trap_grid(48, 10.0) >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi *= 1.0 / np.sqrt(np.sum(np.abs(psi) ** 2) * (X[1, 0] - X[0, 0]) ** 2) >>> E = gpe_energy(psi, V, g=0.0, X=X, Y=Y, K2=K2) >>> round(float(E["kinetic"] + E["potential"]), 4) 1.0
- physicskit.fields.quantum_fields.gpe_evolve(psi0, V, g, dt, steps, K2, snapshot_stride=1)[source]#
Real-time propagation of the 2D Gross-Pitaevskii / cubic NLS equation via split-step Fourier.
Solves \(i\partial_t\psi = [-\tfrac{1}{2}\nabla^2 + V + g|\psi|^2]\psi\) forward in real time (unlike
gpe_relax()’s imaginary-time relaxation, which only finds stationary states): the kinetic term is exact in Fourier space, and the potential-plus-nonlinear term is exact as a pointwise phase rotation in real space, the same split-step structure asphysicskit.fields.solitons.nls_evolve()generalized to 2D and to an external potential. Real-time evolution is unitary and conserves the norm on its own (no renormalization needed, unlike imaginary time).This one stepper serves two purposes, distinguished only by
Vand the sign ofg:With a harmonic trap (
V = 0.5*(X**2+Y**2)) and repulsive interactions (g > 0), an off-center vortex genuinely precesses around the trap under the density gradient – real vortex dynamics, as opposed to the static relaxed stategpe_relax()finds.With
V = 0(or weak) and attractive interactions (g < 0here, opposite sign convention fromnls_evolve’sgsince this module’s equation carries a+g|psi|^2term), a sufficiently tall, narrow initial packet self-focuses, concentrating into a narrower, taller peak – a numerical stand-in for wave collapse. True collapse is a singularity in finite time; this integrator (like any finite-grid scheme) cannot resolve it and the calculation should be stopped once the peak density is still visibly growing, not carried through the blow-up itself.
- Parameters:
psi0 (
ndarray) – Initial wavefunction.V (
ndarray) – External potential (usenp.zeros_likefor the free/collapse case).g (
float) – Interaction strength and sign (see above).dt (
float) – Time step.steps (
int) – Number of steps to advance.K2 (
ndarray) – Wavenumber-squared grid fromharmonic_trap_grid().snapshot_stride (
int) – Record a snapshot every this many steps (plus the initial condition).
- Return type:
- Returns:
snapshots (ndarray of complex, shape (n_recorded, n, n)) – Wavefunction at
t=0and after every recorded step.times (ndarray, shape (n_recorded,)) – Time of each recorded snapshot.
See also
gpe_relaxImaginary-time relaxation to a stationary state.
physicskit.fields.solitons.nls_evolveThe 1D analog (opposite sign convention for
g).
Examples
Norm is conserved under real-time evolution, unlike the imaginary-time propagation in
gpe_relax()(which requires explicit renormalization):>>> import numpy as np >>> n, length = 48, 12.0 >>> X, Y, KX, KY, K2 = harmonic_trap_grid(n, length) >>> dxg = X[1, 0] - X[0, 0] >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi0 *= 1.0 / np.sqrt(np.sum(np.abs(psi0) ** 2) * dxg * dxg) >>> snaps, times = gpe_evolve(psi0, V, g=2.0, dt=1e-3, steps=200, K2=K2, snapshot_stride=50) >>> norms = np.sum(np.abs(snaps) ** 2, axis=(1, 2)) * dxg * dxg >>> bool(np.max(np.abs(norms - norms[0])) < 1e-6) True
- physicskit.fields.quantum_fields.gpe_imprint_vortex(psi, X, Y, positions)[source]#
Imprint one singly-quantized vortex per position by multiplying in a phase winding.
- Parameters:
psi (
ndarray) – Wavefunction to imprint onto.X (
ndarray) – Real-space coordinate grids fromharmonic_trap_grid().Y (
ndarray) – Real-space coordinate grids fromharmonic_trap_grid().positions (sequence of tuple(float, float)) –
(x0, y0)center of each vortex to imprint.
- Return type:
- Returns:
ndarray of complex, shape (n, n) –
psimultiplied by \(\prod_i [(x-x_{0,i}) + i(y-y_{0,i})]\), renormalized to preserve the input norm.
Examples
>>> import numpy as np >>> X, Y, KX, KY, K2 = harmonic_trap_grid(32, 12.0) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi = gpe_imprint_vortex(psi0, X, Y, [(0.0, 0.0)]) >>> bool(abs(psi[16, 16]) < 1e-10) True
- physicskit.fields.quantum_fields.gpe_relax(psi0, V, g, dtau, steps, X, Y, K2, Omega=0.0, n_particles=1.0)[source]#
Relax a GPE initial condition toward a stationary state via imaginary-time propagation.
- Parameters:
psi0 (
ndarray) – Initial wavefunction.V (
ndarray) – External trapping potential.g (
float) – Interaction (nonlinearity) strength.dtau (
float) – Imaginary-time step.steps (
int) – Number of steps to advance.X (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).Y (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).K2 (
ndarray) – Grids fromharmonic_trap_grid()(KX,KYare not needed here).Omega (
float) – Rotation frequency of the trap.n_particles (
float) – Total particle number; the wavefunction is renormalized to this value after every step (imaginary-time propagation does not conserve norm on its own).
- Return type:
- Returns:
ndarray of complex, shape (n, n) – The relaxed (approximately stationary) wavefunction.
See also
gpe_energyEvaluate the energy of a relaxed state.
gpe_imprint_vortexSeed an initial condition with quantized vortices.
Notes
Energy decreases monotonically under this propagation whenever it is implemented correctly (a basic sanity check worth verifying on any new potential or parameter regime).
Examples
Comparing the rotating-frame energy of the vortex-free ground state against a state seeded with one centered vortex reproduces the standard vortex-nucleation criterion: \(\Omega_c = \Delta E/\Delta L_z\), above which the vortex state has lower energy in the rotating frame:
>>> import numpy as np >>> n, length, g = 64, 12.0, 4.0 >>> X, Y, KX, KY, K2 = harmonic_trap_grid(n, length) >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi_vf = gpe_relax(np.exp(-0.5 * (X**2 + Y**2)).astype(complex), V, g, dtau=5e-4, steps=4000, X=X, Y=Y, K2=K2) >>> psi0_v = gpe_imprint_vortex(np.exp(-0.5 * (X**2 + Y**2)).astype(complex), X, Y, [(0.0, 0.0)]) >>> psi_v = gpe_relax(psi0_v, V, g, dtau=5e-4, steps=4000, X=X, Y=Y, K2=K2) >>> E0, E1 = gpe_energy(psi_vf, V, g, X, Y, K2), gpe_energy(psi_v, V, g, X, Y, K2) >>> Omega_c = (E1["total"] - E0["total"]) / (E1["angular_momentum"] - E0["angular_momentum"]) >>> bool(0.0 < Omega_c < 1.0) True
- physicskit.fields.quantum_fields.harmonic_trap_grid(n, length)[source]#
Build a centered real-space and wavenumber grid for a 2D harmonic trap.
- Parameters:
- Return type:
- Returns:
X, Y (ndarray, shape (n, n)) – Real-space coordinate grids, centered at the trap origin.
KX, KY (ndarray, shape (n, n)) – Wavenumber grids.
K2 (ndarray, shape (n, n)) – \(K_X^2 + K_Y^2\).
Examples
>>> X, Y, KX, KY, K2 = harmonic_trap_grid(32, 12.0) >>> float(X[0, 0]) -6.0
Plotting helpers for electrodynamics, soliton, and BEC fields.
Every function returns its figure object rather than calling show().
- physicskit.fields.visualizers.animate_casimir_modes(d_values, c=1.0, n_show=12, interval=150)[source]#
Animate the discrete cavity mode spectrum and Casimir energy as the plate separation
dis swept.Left panel: the first
n_showdiscrete mode frequencies (physicskit.fields.quantum_fields.casimir_mode_frequencies()) as a stem plot, against the continuum they approach at larged(dashed line of slope \(\pi c\)) – visibly denser (closer to the continuum) at large separation. Right panel: the regularized Casimir energy (physicskit.fields.quantum_fields.casimir_energy_1d()) as a curve over the full sweep, with a marker tracking the currentd.- Parameters:
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> anim = animate_casimir_modes(np.linspace(1.0, 5.0, 5)) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.visualizers.animate_density_2d(frames, extent=None, times=None, interval=50, cmap='viridis', ax=None)[source]#
Animate a sequence of non-negative 2D density snapshots (e.g. BEC or flux-tube energy density) as an imshow heatmap.
- Parameters:
frames (
ndarray) – Snapshots. Complex frames (a wavefunction) are converted to \(|\psi|^2\); real frames are used directly as the density/energy map.extent (
tuple|None) –(xmin, xmax, ymin, ymax)passed toimshow; defaults to pixel indices.times (
ndarray|None) – Time (or other sweep parameter, e.g. propagation distance) of each frame.interval (
int) – Delay between frames, in milliseconds.cmap (
str) – Colormap.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> from physicskit.fields.quantum_fields import harmonic_trap_grid, gpe_imprint_vortex, gpe_evolve >>> n, length = 32, 12.0 >>> X, Y, KX, KY, K2 = harmonic_trap_grid(n, length) >>> V = 0.5 * (X ** 2 + Y ** 2) >>> psi0 = gpe_imprint_vortex(np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex), X, Y, [(1.0, 0.0)]) >>> frames, times = gpe_evolve(psi0, V, g=2.0, dt=1e-3, steps=20, K2=K2, snapshot_stride=5) >>> anim = animate_density_2d(frames, times=times) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.visualizers.animate_field_1d(x, frames, times=None, interval=50, ylabel='u(x, t)', ax=None)[source]#
Animate a sequence of 1D field snapshots as a line plot.
Shared by the KdV, NLS, and Sine-Gordon evolvers: feed it the frames from
physicskit.fields.solitons.kdv_evolve_frames(),nls_evolve_frames(), orsine_gordon_evolve_frames().- Parameters:
x (
ndarray) – Spatial grid.frames (
ndarray) – Field snapshots. Complex frames (an NLS wavefunction) are plotted as \(|\psi|\); real frames (KdV, Sine-Gordon) are plotted directly.times (
ndarray|None) – Time of each frame, shown in the title.interval (
int) – Delay between frames, in milliseconds.ylabel (
str) – Y-axis label.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> from physicskit.fields.solitons import kdv_soliton, kdv_evolve_frames >>> x = np.linspace(-30, 30, 256, endpoint=False) >>> frames, times = kdv_evolve_frames(kdv_soliton(x, c=4.0, x0=-15), x, dt=0.001, steps_per_frame=50, n_frames=3) >>> anim = animate_field_1d(x, frames, times) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.visualizers.animate_field_2d(X, Y, frames, times=None, interval=50, cmap='RdBu_r', ax=None)[source]#
Animate a sequence of 2D scalar field snapshots (e.g. FDTD
Ez) as an imshow heatmap.Uses a diverging colormap centered at zero, symmetric about the largest-magnitude value across all frames – appropriate for an oscillating field like
Ez, unlike the non-negative densities handled byanimate_density_2d().- Parameters:
X (
ndarray) – Real-space coordinate grids (used only for their extent).Y (
ndarray) – Real-space coordinate grids (used only for their extent).frames (
ndarray) – Field snapshots, e.g. fromphysicskit.fields.electrodynamics.fdtd_2d_tmz_evolve().times (
ndarray|None) – Time of each frame, shown in the title.interval (
int) – Delay between frames, in milliseconds.cmap (
str) – Diverging colormap.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> from physicskit.fields.electrodynamics import ( ... courant_limit_2d, fdtd_2d_tmz_evolve, oscillating_dipole_source) >>> Nx, Ny = 30, 30 >>> x = np.arange(Nx) * 1e-3 >>> y = np.arange(Ny) * 1e-3 >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> Ez0 = Hx0 = Hy0 = np.zeros((Nx, Ny)) >>> eps_r = mu_r = np.ones((Nx, Ny)) >>> dt = 0.5 * courant_limit_2d(1e-3, 1e-3) >>> source = oscillating_dipole_source(Nx // 2, Ny // 2, amplitude=1.0, freq=5e10) >>> frames, times = fdtd_2d_tmz_evolve(Ez0, Hx0, Hy0, eps_r, mu_r, steps=10, dt=dt, dx=1e-3, dy=1e-3, source=source, snapshot_stride=2) >>> anim = animate_field_2d(X, Y, frames, times) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.visualizers.animate_flux_tube(shape, dx, dy, separations, flux_quantum=1.0, interval=80, ax=None)[source]#
Animate the toy confinement flux tube stretching as two charges are pulled apart.
Builds a frame for each separation in
separationsviaphysicskit.fields.electrodynamics.flux_tube_energy_density_2d()and draws it as a heatmap with two markers tracking the charge positions, so the confined-energy “tube” visibly stretches between them.- Parameters:
shape (
tuple) –(Nx, Ny)grid shape.dx (
float) – Grid spacing.dy (
float) – Grid spacing.separations (
ndarray) – Sequence of charge separations to sweep over (the animation’s “time” axis).flux_quantum (
float) – Charge magnitude, as influx_tube_field_1d().interval (
int) – Delay between frames, in milliseconds.ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
- Return type:
FuncAnimation- Returns:
matplotlib.animation.FuncAnimation
Examples
>>> import numpy as np >>> anim = animate_flux_tube((120, 30), dx=0.2, dy=0.2, separations=np.linspace(4.0, 16.0, 5)) >>> isinstance(anim, FuncAnimation) True
- physicskit.fields.visualizers.plot_bec_density(X, Y, psi, ax=None)[source]#
Plot the condensate density \(|\psi(\mathbf{r})|^2\) as a heatmap.
- Parameters:
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
See also
plot_bec_phasePlot the phase, which reveals vortex cores as singularities.
Examples
>>> import numpy as np >>> from physicskit.fields.quantum_fields import harmonic_trap_grid >>> X, Y, KX, KY, K2 = harmonic_trap_grid(40, 10.0) >>> psi = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> fig, ax = plot_bec_density(X, Y, psi) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.visualizers.plot_bec_phase(X, Y, psi, ax=None)[source]#
Plot the condensate phase \(\arg\psi(\mathbf{r})\); vortex cores appear as \(2\pi\) singularities.
- Parameters:
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
See also
plot_bec_densityPlot the density, which shows vortex cores as zeros.
physicskit.fields.quantum_fields.count_vorticesQuantitative vortex detection.
Examples
>>> import numpy as np >>> from physicskit.fields.quantum_fields import harmonic_trap_grid, gpe_imprint_vortex >>> X, Y, KX, KY, K2 = harmonic_trap_grid(40, 10.0) >>> psi0 = np.exp(-0.5 * (X ** 2 + Y ** 2)).astype(complex) >>> psi = gpe_imprint_vortex(psi0, X, Y, [(0.0, 0.0)]) >>> fig, ax = plot_bec_phase(X, Y, psi) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.visualizers.plot_field_1d(x, u, ax=None, label=None)[source]#
Plot a 1D field snapshot (a KdV, NLS envelope, or Sine-Gordon profile).
- Parameters:
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
Examples
>>> import numpy as np >>> from physicskit.fields.solitons import kdv_soliton >>> x = np.linspace(-20, 20, 200) >>> fig, ax = plot_field_1d(x, kdv_soliton(x, c=4.0)) >>> isinstance(fig, plt.Figure) True
- physicskit.fields.visualizers.plot_poynting_field(X, Y, Sx, Sy, ax=None, stride=4)[source]#
Quiver-plot the Poynting energy-flux field over a 2D grid.
- Parameters:
X (
ndarray) – Real-space coordinate grids.Y (
ndarray) – Real-space coordinate grids.Sx (
ndarray) – Poynting vector components, as fromphysicskit.fields.electrodynamics.poynting_vector_tmz().Sy (
ndarray) – Poynting vector components, as fromphysicskit.fields.electrodynamics.poynting_vector_tmz().ax (matplotlib.axes.Axes, optional) – Axes to draw into; a new figure is created if omitted.
stride (
int) – Subsample everystride-th grid point, to keep the quiver plot legible.
- Returns:
fig (matplotlib.figure.Figure)
ax (matplotlib.axes.Axes)
Examples
>>> import numpy as np >>> X, Y = np.meshgrid(np.linspace(-1, 1, 20), np.linspace(-1, 1, 20)) >>> Sx, Sy = -Y, X >>> fig, ax = plot_poynting_field(X, Y, Sx, Sy) >>> isinstance(fig, plt.Figure) True