This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize
for the AI-speak, we are working on rewriting it!
Fourier pseudo-spectral in space; Strang splitting in time, with the dispersive step exact
Checked
a soliton against its exact solution: shape, speed, conserved mass, and the measured order
Shallow-water waves, and the equation solitons were discovered in. Nonlinearity steepens a hump; third-order dispersion spreads it; and for exactly one family of shapes the two cancel: u=2csech2(2c(x−ct−x0)). Taller solitons are narrower and faster — the speed c is twice the height.
uxxx is the hard part for finite differences: an explicit scheme needs Δt∼Δx3. In Fourier space it is trivial — each mode just turns, u^k→u^keik3Δt, exactly, for any Δt. So split: half a dispersive step, a full step of the nonlinear part ut=−3(u2)x (spectral derivative, fourth-order Runge-Kutta), half a dispersive step.
The Nyquist mode is zeroed in the derivative. On an even grid it has no well-defined sign, and leaving it in makes the derivative of a real field complex.
What the run shows
A soliton of speed 2: it moves, and nothing else happens
t = 0t = 2, computedt = 2, exact
Two solitons collide and pass through each other
t
x
00.51
u
41 × 128 grid
✓the soliton keeps its shape
✓it moves at c, twice its height
✓mass is conserved to rounding: a spectral derivative has exactly zero mean
✓the splitting is second order
✓after the collision the two solitons are Hirota's exact solution, phase shifts included
shape_error
3.89994e-06
mass_drift
5.55112e-16
speed
2
order
2.01469
collision_error
0.00031525
Computed when this page was built, by the same interpreter that runs it in your browser: 5 of 5 tests,
0.28 s. The numbers above are the program's show lines.
The program
163 lines, 6 functions. It is editable: change it and press Run (or
⌘/Ctrl + Enter). The tests at the bottom run too, so if your change breaks the solver, a test fails.
soliton
The soliton of speed C at time t.
nonlinear_rate
The nonlinear rate, -3 d/dx (u^2), with the derivative taken in Fourier space.
disperse
The dispersive step, exact: every mode turns by exp(i k^3 dt).
march
March the soliton to t_end in `steps` Strang steps.
center
Where the hump is: its center of mass. A sum over a periodic grid is spectrally accurate for a pulse that has decayed before the edges, so this locates it far below the spacing (a parabola through the top three points is off by 2e-4 here, and no time step fixes that).
two_solitons
Hirota's exact two-soliton solution on the grid X at time t.
Viv [1]
pi =3.141592653589793
N =256
L =40.0
DX = L / N
X =linspace(-0.5* L, 0.5* L - DX, N)
K =zeros(N)
for j to N {
K[j] =2.0* pi * j / L
if j >= N /2.0 { K[j] =2.0* pi * (j - N) / L }
}
K[N /2.0] =0.0
ZERO =zeros(N)
IK =complex(ZERO, K)
C =2.0
X0 =-6.0#| The soliton of speed C at time t.defsoliton(t) {
s =1.0/cosh(0.5*sqrt(C) * (X - C * t - X0))
return0.5* C * s * s
}
#| The nonlinear rate, -3 d/dx (u^2), with the derivative taken in Fourier space.defnonlinear_rate(u) {
d =complex(u * u, ZERO)
fft(d)
d = d * IK
ifft(d)
return-3.0*re(d)
}
#| The dispersive step, exact: every mode turns by exp(i k^3 dt).defdisperse(u, dt) {
v =complex(u, ZERO)
fft(v)
theta = K * K * K * dt
v = v *complex(cos(theta), sin(theta))
ifft(v)
returnre(v)
}
#| March the soliton to t_end in `steps` Strang steps.defmarch(t_end, steps) {
dt = t_end / steps
u =soliton(0.0)
for s to steps {
u =disperse(u, 0.5* dt)
k1 =nonlinear_rate(u)
k2 =nonlinear_rate(u +0.5* dt * k1)
k3 =nonlinear_rate(u +0.5* dt * k2)
k4 =nonlinear_rate(u + dt * k3)
u = u + (dt /6.0) * (k1 +2.0* k2 +2.0* k3 + k4)
u =disperse(u, 0.5* dt)
}
return u
}
#| Where the hump is: its center of mass. A sum over a periodic grid is spectrally accurate#| for a pulse that has decayed before the edges, so this locates it far below the spacing#| (a parabola through the top three points is off by 2e-4 here, and no time step fixes that).defcenter(u) {
returnsum(X * u) /sum(u)
}
T_END =2.0
u =march(T_END, 1600.0)
d = u -soliton(T_END)
shape_error =sqrt(sum(d * d) * DX)
mass_drift =abs(sum(u) -sum(soliton(0.0))) * DX
speed = (center(u) - X0) / T_END
d1 =march(T_END, 200.0) -soliton(T_END)
d2 =march(T_END, 400.0) -soliton(T_END)
order =log(sqrt(sum(d1 * d1)) /sqrt(sum(d2 * d2))) /log(2.0)
show shape_error
show mass_drift
show speed
show order
figure("A soliton of speed 2: it moves, and nothing else happens", "x", "u")
plot(X, soliton(0.0), "t = 0")
plot(X, u, "t = 2, computed")
plot(X, soliton(T_END), "t = 2, exact")
style("t = 2, exact", "dash")
test"the soliton keeps its shape" {
assert_lt(shape_error, 1.0e-5)
}
test"it moves at c, twice its height" {
assert_near(speed, C, 1.0e-4)
}
test"mass is conserved to rounding: a spectral derivative has exactly zero mean" {
assert_lt(mass_drift, 1.0e-12)
}
test"the splitting is second order" {
assert_near(order, 2.0, 0.1)
}
# Two solitons: the taller one starts behind, catches the shorter, and passes through it.# Hirota's exact two-soliton solution is u = 2 (log tau)_xx with# tau = 1 + e^eta1 + e^eta2 + A e^(eta1 + eta2), eta_i = k_i (x - x_i) - k_i^3 t and# A = ((k1 - k2) / (k1 + k2))^2; soliton i has speed k_i^2 and height k_i^2 / 2.# After the collision each is where it would be, shifted: the taller ahead, the shorter behind.
K1 =1.6
K2 =1.0
X1 =-13.0
X2 =-5.0
T2_END =8.0
T2_STEPS =1600# a row of the waterfall every this many steps, every other grid point
T2_EVERY =40
X_STRIDE =2#| Hirota's exact two-soliton solution on the grid X at time t.#@ 5cc5e18adeftwo_solitons(t) {
A = (K1 - K2) * (K1 - K2) / ((K1 + K2) * (K1 + K2))
e1 =exp(K1 * (X - X1) - K1 * K1 * K1 * t)
e2 =exp(K2 * (X - X2) - K2 * K2 * K2 * t)
e12 = A * e1 * e2
tau =1.0+ e1 + e2 + e12
d1 = K1 * e1 + K2 * e2 + (K1 + K2) * e12
d2 = K1 * K1 * e1 + K2 * K2 * e2 + (K1 + K2) * (K1 + K2) * e12
return2.0* (d2 * tau - d1 * d1) / (tau * tau)
}
n_rows = T2_STEPS / T2_EVERY +1
n_cols = N / X_STRIDE
waterfall =zeros(n_rows, n_cols)
w =two_solitons(0.0)
dt2 = T2_END / T2_STEPS
for s to T2_STEPS +1 {
ifmod(s, T2_EVERY) ==0 {
for c to n_cols {
waterfall[s / T2_EVERY, c] =round(1.0e4* w[c * X_STRIDE]) /1.0e4
}
}
if s < T2_STEPS {
w =disperse(w, 0.5* dt2)
q1 =nonlinear_rate(w)
q2 =nonlinear_rate(w +0.5* dt2 * q1)
q3 =nonlinear_rate(w +0.5* dt2 * q2)
q4 =nonlinear_rate(w + dt2 * q3)
w = w + (dt2 /6.0) * (q1 +2.0* q2 +2.0* q3 + q4)
w =disperse(w, 0.5* dt2)
}
}
dw = w -two_solitons(T2_END)
collision_error =max(abs(dw))
show collision_error
figure("Two solitons collide and pass through each other", "x", "t", -0.5* L, 0.5* L - DX, 0.0, T2_END)
origin("lower")
plot2d(waterfall, "u")
test"after the collision the two solitons are Hirota's exact solution, phase shifts included" {
# the splitting's second-order error at dt = 0.005 is 3.2e-4; at dt = 0.0025 it is 7.8e-5assert_lt(collision_error, 5.0e-4)
}
# SPDX-License-Identifier: MIT OR Apache-2.0# SPDX-FileCopyrightText: 2026 The Viv authors