Energy conservation: symplectic vs RK4
Symplectic integrators keep total energy bounded forever; RK4's energy walks off in one direction despite higher local accuracy.
Eccentric two-body (e=0.7), dt=0.01, 40k steps, G=1 method max |dE/E| net dE/E drift shape ------------------------------------------------------------------------ verlet 4.916e-03 -1.112e-07 # @ # # * * + = = - : : . . . forest_ruth 1.080e-05 1.114e-11 # @ # # * * + + = - - : : : . . . . rk4 1.379e-04 1.379e-04 ........:::::::-------========++++++++*******#######@ Symplectic methods (verlet, forest_ruth) oscillate around 0. RK4's error walks off in one direction -- that's secular energy drift.
Convergence order (vs the exact Kepler orbit)
Halving the step cuts verlet's error 4x (order 2) and forest_ruth/rk4's error 16x (order 4) -- measured, not assumed.
Convergence to the exact Kepler orbit (e=0.5, one period, G=1)
error = max position error at t=T vs analytic solution
verlet:
steps error ratio order
500 4.500e-04 -- --
1000 1.125e-04 4.00 2.00
2000 2.813e-05 4.00 2.00
4000 7.034e-06 4.00 2.00
8000 1.758e-06 4.00 2.00
forest_ruth:
steps error ratio order
500 8.095e-07 -- --
1000 5.069e-08 15.97 4.00
2000 3.170e-09 15.99 4.00
4000 1.981e-10 16.00 4.00
8000 1.242e-11 15.95 4.00
rk4:
steps error ratio order
500 9.910e-08 -- --
1000 5.798e-09 17.09 4.10
2000 3.498e-10 16.57 4.05
4000 2.146e-11 16.30 4.03
8000 1.350e-12 15.90 3.99
verlet doubles-steps -> ~4x smaller error (order 2).
forest_ruth & rk4 -> ~16x smaller error (order 4).
Same order, but forest_ruth is symplectic: it ALSO keeps energy
bounded forever, which rk4 does not (see energy_drift_demo.py).
Hermite: 4th order at one force call per step
RK4 and Forest-Ruth reach 4th order at 4 and 3 force calls per step; the Hermite predictor-corrector reaches it with a single force+jerk call, so for a fixed force budget it takes more steps and lands far more accurate. It's the integrator real star-cluster codes use.
Accuracy vs force-evaluation budget (exact Kepler orbit, e=0.5) method steps f-evals end error order ------------------------------------------------------ rk4 6000 24000 4.230e-12 4.10 forest_ruth 8000 24000 1.242e-11 4.00 hermite 24000 24000 1.964e-13 4.01 All three are 4th order, but for the SAME force-evaluation budget Hermite takes 4x as many steps as RK4 (1 call/step vs 4), so it reaches a much smaller error. That efficiency is why Hermite runs star clusters.
Adaptive stepping: same accuracy, far less work
Dormand-Prince RK45 spends tiny steps at pericenter and long steps at apocenter, matching fixed-RK4 accuracy with ~9x fewer force evals.
Eccentric two-body (e=0.7), one full period T=4.4429, G=1
adaptive step size over the orbit (small=pericenter, large=apocenter):
.=+**###@##*+==---::..::::::........ ... .............::::::::::--===++*#######*-
steps accepted=109 rejected=13 h_min=1.00e-03 h_max=1.17e-01 ratio=117x
method force evals end error
----------------------------------------------------
adaptive DP45 854 3.95e-08
fixed RK4 (2000 steps) 8000 8.02e-09
adaptive reaches the same accuracy with 9.4x fewer force evaluations.
Barnes-Hut scaling
An octree collapses distant bodies to their centre of mass, turning O(N^2) direct summation into a sub-quadratic force evaluation.
Force-evaluation time: direct O(N^2) vs Barnes-Hut O(N log N), theta=0.5
N direct (ms) bh (ms) speedup
---------------------------------------------
200 14.88 19.75 0.8x
400 60.72 46.69 1.3x
800 192.12 132.34 1.5x
1600 655.16 369.68 1.8x
3200 2595.83 1003.31 2.6x
empirical scaling exponent direct ~ N^1.83 barnes-hut ~ N^1.43
direct sits at ~2.0 (quadratic). barnes-hut is clearly sub-quadratic;
its exponent falls toward the N log N asymptote as N grows and the
per-call tree-build overhead is amortized over more force terms.
Orbit gallery
Trajectories integrated and rendered to dependency-free SVG. The animated versions move the bodies along their paths via SMIL (no JS).
The real solar system & Kepler's third law
Eight planets from published orbital elements, in AU/years/solar masses. T^2/a^3 comes out constant -- Kepler's third law, straight from Newtonian gravity and a symplectic integrator, no fitting.
Solar system in AU / years / solar masses (G = 4*pi^2) planet a [AU] T measured T Kepler T real T^2/a^3 ------------------------------------------------------------- Mercury 0.387 0.2410 0.2408 0.2408 1.0010 Venus 0.723 0.6155 0.6152 0.6152 1.0010 Earth 1.000 1.0005 1.0000 1.0000 1.0010 Mars 1.524 1.8817 1.8808 1.8808 1.0010 Jupiter 5.204 11.9080 11.8724 11.8620 1.0060 Saturn 9.582 29.6906 29.6610 29.4570 1.0020 Uranus 19.189 84.1010 84.0589 84.0210 1.0010 Neptune 30.070 164.9740 164.8916 164.7900 1.0010 T^2/a^3 is constant across all planets -- that constant is 1 in these units, which IS Kepler's third law. It falls straight out of Newtonian gravity + a symplectic integrator, no fitting. wrote C:\Users\acwic\symplectic-nbody\examples\output\inner_planets.svg
Exoplanet detection: transits & radial velocity
Two methods, both simple geometry + Kepler: a transit dims the star by (R_p/R_star)^2 (Jupiter ~1%, Earth 0.008%), and the star wobbles by a radial-velocity K (Jupiter 12 m/s, Earth 9 cm/s). Hot Jupiters give the biggest signals -- which is why they were found first.
Exoplanet detection: transit depth and radial-velocity wobble planet a (AU) depth RV K (m/s) period ------------------------------------------------------------- hot Jupiter 0.050 1.01e-02 127.07 4.1 d Jupiter 5.204 1.01e-02 12.46 11.9 yr Earth 1.000 8.39e-05 0.09 365.2 d A transiting Jupiter dims its star ~1%; an Earth only 0.008%. Jupiter wobbles the Sun 12 m/s, Earth just 9 cm/s. Hot Jupiters -- big, close, fast -- give the strongest signals, which is why they were found first. wrote C:\Users\acwic\symplectic-nbody\examples\output\exoplanet.svg
The habitable zone: where liquid water survives
A planet's equilibrium temperature T_eq ~ L^1/4 / sqrt(d) sets the band of orbits where water stays liquid. Earth's T_eq is 255 K (greenhouse warms it to 288 K). The zone marches out as sqrt(L) -- close in for red dwarfs, far out for luminous stars.
The habitable zone: liquid-water orbits around a star Earth's equilibrium temperature: 255 K (greenhouse warms the surface to ~288 K) star L (L_sun) HZ inner HZ outer --------------------------------------------------- red dwarf (0.3) 0.01 0.06 AU 0.11 AU Sun (1.0) 1.00 0.47 AU 0.87 AU F star (1.5) 4.13 0.95 AU 1.77 AU A star (2.0) 11.31 1.57 AU 2.93 AU The zone marches out as sqrt(L): red-dwarf HZs hug the star (and risk tidal locking), while luminous stars push it far out. This is the target band for finding worlds that could have surface oceans. wrote C:\Users\acwic\symplectic-nbody\examples\output\habitable_zone.svg
Gravitational focusing & runaway growth
Colliding bodies don't need a direct hit -- gravity bends distant trajectories in, enhancing the cross-section by 1 + v_esc^2/v_inf^2. In a cold planetesimal swarm this makes the biggest bodies grow fastest (runaway growth), seeding planetary embryos.
Gravitational focusing (100 km planetesimal)
escape speed: 130 m/s
v_inf (m/s) focusing Safronov regime
------------------------------------------------
1 16775.3 8387.17 runaway
10 168.7 83.87 runaway
50 7.7 3.35 runaway
100 2.7 0.84 geometric
500 1.1 0.03 geometric
1000 1.0 0.01 geometric
When the swarm is dynamically cold (v_inf << v_esc) gravity bends
distant trajectories into collisions, so the biggest bodies grow
fastest -- runaway growth that seeds planetary embryos. Stir the swarm
up (high v_inf) and only direct hits count: growth slows to geometric.
wrote C:\Users\acwic\symplectic-nbody\examples\output\focusing.svgHulse-Taylor binary pulsar (GW before LIGO)
PSR B1913+16's orbit shrinks as it radiates gravitational waves; the predicted period decay dP/dt = -2.40e-12 s/s matches the measured value to 99%. Tracking the cumulative shift for decades won the 1993 Nobel Prize, 22 years before LIGO's direct detection.
PSR B1913+16 (Hulse-Taylor): gravitational waves before LIGO orbital period : 7.752 hours eccentricity : 0.6171334 dP/dt (GR predicted): -2.4031e-12 s/s dP/dt (measured) : -2.423e-12 s/s agreement : 99.2% of measured cumulative shift over 30 yr : -38.6 s P/|dP/dt| timescale : 368 Myr The orbit's period shrinks by ~76 microseconds per year as the system radiates gravitational waves. Tracking that decay for decades -- the parabola below -- won the 1993 Nobel Prize, 22 years before LIGO. wrote C:\Users\acwic\symplectic-nbody\examples\output\pulsar.svg
Gravitational-wave inspiral (the LIGO chirp)
A 2.5PN radiation-reaction force bleeds orbital energy into gravitational waves. The binary spirals inward and its frequency chirps upward -- the same mechanism as GW150914. The energy-loss rate matches Peters (1964) to a few percent.
Gravitational-wave inspiral (equal-mass binary, GR amplified: v/c~0.20)
energy-loss rate dE/dt: measured -1.426e-04 Peters -1.243e-04 ratio 1.147
separation: 1.00 -> 0.38 (orbit shrinks as it radiates)
orbital frequency chirps up 4.2x:
...............:::::::---==+*@
wrote C:\Users\acwic\symplectic-nbody\examples\output\gw_inspiral.svg
Same mechanism as GW150914: the binary radiates orbital energy as
gravitational waves and spirals to merger, chirping as it goes.GW strain: the number LIGO measures
The wave's amplitude h ~ (G M_c/c^2)^{5/3}(pi f/c)^{2/3}/d. For GW150914 (chirp mass ~28 M_sun, 410 Mpc) that is h ~ 1e-21, moving LIGO's 4 km arms by ~1e-18 m -- a thousandth of a proton's width.
Gravitational-wave strain: measuring a proton-width in kilometers
GW150914 (36 + 29 M_sun, 410 Mpc, f_gw~150 Hz):
chirp mass = 28.1 M_sun
strain h = 2.13e-21
LIGO arm move = 8.50e-18 m (1.1% of a proton width)
binary distance strain h
--------------------------------------------------
GW150914 (36+29) 410 Mpc 2.1e-21
neutron stars (1.4+1.4) 130 Mpc 3.6e-23
supermassive (1e6+1e6) 10000 Mpc 2.7e-15
A strain of 1e-21 moves LIGO's 4 km arms by ~1e-18 m -- a thousandth
of a proton's width. Detecting it is why LIGO is one of the most
sensitive instruments ever built.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gw_strain.svgGravitational waves circularize binaries (Peters 1964)
The coupled Peters (a, e) equations: as a binary radiates, both its size and its eccentricity shrink, with e falling faster near merger. Every orbit -- even a wildly eccentric one -- is nearly circular by the time it merges.
Peters (1964): gravitational waves circularize binaries
e0 a_final e_final e reduction
--------------------------------------------
0.20 0.0098 0.0001 1406.2x
0.40 0.0099 0.0004 1093.2x
0.60 0.0098 0.0009 688.1x
0.80 0.0099 0.0031 259.9x
0.90 0.0099 0.0099 90.9x
wrote C:\Users\acwic\symplectic-nbody\examples\output\circularization.svg
Every track bends toward e=0: by merger, even a wildly eccentric
binary is nearly circular. That's why LIGO templates start circular.Galaxy collision & tidal tails
Two disk galaxies (heavy cores + cold tracer disks) pass close, and differential gravity draws their disks into bridges and tails -- the same physics as the Antennae and the Mice. Barnes-Hut forces, ~1000 bodies. Frames left-to-right in time.
Two-galaxy encounter: 1002 bodies, Barnes-Hut forces wrote C:\Users\acwic\symplectic-nbody\examples\output\galaxy_t0.svg (t=0.0) wrote C:\Users\acwic\symplectic-nbody\examples\output\galaxy_t1.svg (t=4.0) wrote C:\Users\acwic\symplectic-nbody\examples\output\galaxy_t2.svg (t=8.0) wrote C:\Users\acwic\symplectic-nbody\examples\output\galaxy_t3.svg (t=12.0) wrote C:\Users\acwic\symplectic-nbody\examples\output\galaxy_t4.svg (t=16.0) 487 tracer particles pulled into tidal bridges/tails. Same physics as the Antennae and the Mice: differential gravity stretches a cold disk into long tails during a close passage.
Dynamical friction: satellites spiralling in
A massive body moving through a star field pulls a wake behind it that drags it back (Chandrasekhar friction ~ M^2 rho / v^2). Satellites and globular clusters spiral into their host on a time that scales as 1/M -- heavier sinks faster, dragging black holes to centres.
Chandrasekhar dynamical friction: satellites spiralling in
sinking time into a Milky-Way-like halo (r0=50 kpc, v_circ=220 km/s):
satellite mass sinking time
----------------------------------
globular cluster (1e+08) 292.55 Gyr
LMC-scale (1e+10) 2.93 Gyr
massive dwarf (1e+11) 0.29 Gyr
Heavier objects sink faster (t ~ 1/M): a massive satellite merges in a
few Gyr, while a light globular cluster survives ~a Hubble time. This
drags massive black holes to galactic centres and erodes cluster orbits.
wrote C:\Users\acwic\symplectic-nbody\examples\output\dynamical_friction.svgSitnikov problem: a clean route to chaos
A massless body on the z-axis through a binary. For a circular binary the stroboscopic map is smooth nested curves (integrable); give the binary eccentricity and the inner curves shred into a chaotic layer -- the system Moser used to prove chaos exists.
Sitnikov problem: stroboscopic Poincare maps (sample once per binary period) e=0.0: 10 orbits survived, 2500 section points, mean z-spread 1.83 wrote C:\Users\acwic\symplectic-nbody\examples\output\sitnikov_circ.svg e=0.3: 10 orbits survived, 2500 section points, mean z-spread 198.81 wrote C:\Users\acwic\symplectic-nbody\examples\output\sitnikov_ecc.svg e=0 draws smooth nested curves (each orbit lies on an invariant torus). e=0.3 tears the inner curves into a chaotic sea -- the Sitnikov route to chaos.
Poincare surface-of-section
Many orbits at the SAME Jacobi energy, their y=0 crossings overlaid on the (x, vx) plane. Smooth closed loops are quasi-periodic KAM tori; the scattered dust is chaos -- coexisting at one energy.
Poincare section of the CR3BP (mu=0.1, Jacobi C=3.9, surface y=0) integrated 19 orbits; 2 trace tight closed curves (KAM tori), the rest fill chaotic regions. wrote C:\Users\acwic\symplectic-nbody\examples\output\poincare_section.svg
Sedov-Taylor blast wave
A point energy release drives a self-similar shock, R ~ (E t^2/rho)^1/5. The same law dates supernova remnants (pc-scale, thousands of km/s) and -- run backwards -- let G. I. Taylor weigh the Trinity bomb from a movie of the fireball while its yield was still classified.
Sedov-Taylor blast wave
Trinity test (Taylor's declassification trick):
fireball R=130 m at t=25 ms in air -> yield ~ 9 kilotons
(the actual device was ~21 kt -- right order from a movie frame)
Supernova remnant (E=1e51 erg, ISM ~1 H/cc):
age (yr) radius (pc) shock (km/s) T (K)
--------------------------------------------------
100 2.05 8018 8.8e+08
300 3.18 4148 2.3e+08
1000 5.15 2014 5.5e+07
3000 7.99 1042 1.5e+07
10000 12.93 506 3.5e+06
R grows as t^2/5 and the shock decelerates as t^-3/5. The same self-
similar law dates supernova remnants and (run backwards) weighed the bomb.
wrote C:\Users\acwic\symplectic-nbody\examples\output\sedov.svgJeans instability: the birth of a star
A gas cloud collapses when gravity beats pressure. The dispersion relation omega^2 = c_s^2 k^2 - 4 pi G rho splits into stable sound waves (short wavelength) and collapsing modes (long wavelength) at the Jeans length -- the threshold for all star and structure formation.
Jeans instability: the collapse threshold (c_s = rho0 = G = 1)
Jeans wavenumber k_J : 3.5449
Jeans length : 1.7725
Jeans mass : 2.9156
free-fall time : 0.5427
k/k_J omega^2 behaviour
-----------------------------------
0.25 -11.781 collapse
0.50 -9.425 collapse
0.75 -5.498 collapse
1.00 -0.000 collapse
1.50 15.708 sound wave
2.00 37.699 sound wave
Below k_J (long wavelength / large cloud) gravity beats pressure and
the gas collapses -- the birth of a star. Above it, pressure wins and
the perturbation is just a sound wave.
wrote C:\Users\acwic\symplectic-nbody\examples\output\jeans.svgNeutron stars & the TOV maximum mass
For a neutron star, gravity is strong enough that Newtonian hydrostatics fails -- you need the relativistic TOV equation. The mass-radius curve turns over at a maximum mass (no static star above it), while the Newtonian version has no limit. GR makes black holes possible.
Neutron-star structure: the TOV maximum mass
rho_c R (km) M_TOV M_Newton
------------------------------------------
1.0e-03 10.37 0.946 1.70
2.3e-03 8.80 1.274 3.89
5.2e-03 7.09 1.342 8.91
1.2e-02 5.77 1.211 20.41
2.8e-02 5.10 1.066 46.75
6.3e-02 4.92 0.991 107.11
1.4e-01 4.92 0.969 245.37
3.3e-01 4.93 0.965 562.11
7.6e-01 4.94 0.965 1287.71
TOV maximum mass : 1.351 M_sun (the sequence turns over)
Newtonian, densest star: 1698 M_sun (no limit -- grows forever)
General relativity imposes a maximum neutron-star mass. Above it the
star cannot support itself and collapses to a black hole.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tov.svgFermi degeneracy pressure
The Pauli principle makes a cold, dense electron gas resist compression -- the quantum pressure that supports white dwarfs. It softens from P ~ n^5/3 to P ~ n^4/3 as electrons turn relativistic, and that softer exponent is the seed of the Chandrasekhar mass.
Fermi degeneracy pressure: the quantum floor under dead stars
relativistic transition density: 5.87e+35 /m^3
n (/m^3) regime P (Pa)
----------------------------------------------
1e+29 non-rel 5.03e+10
1e+32 non-rel 5.03e+15
1e+35 non-rel 5.03e+20
1e+36 relativistic 2.45e+22
1e+38 relativistic 1.13e+25
Below the transition the pressure goes as n^5/3; above it, as n^4/3.
That softer exponent is exactly why gravity eventually wins in a
massive white dwarf -- the origin of the Chandrasekhar mass.
wrote C:\Users\acwic\symplectic-nbody\examples\output\degeneracy.svgThe Chandrasekhar mass
White dwarfs are held up by electron degeneracy pressure. Integrating the full relativistic degenerate equation of state, the mass climbs toward a hard limit ~1.44 M_sun as the star shrinks -- above it no white dwarf is stable, the trigger for type-Ia supernovae.
White-dwarf structure and the Chandrasekhar limit
n=3 Lane-Emden mass factor : 2.01824
Chandrasekhar mass (mu_e=2): 1.435 M_sun (the famous 1.44)
rho_c (kg/m^3) radius (km) mass (M_sun)
--------------------------------------------
3.2e+08 13220 0.246
1.0e+09 10780 0.395
3.2e+09 8680 0.588
1.0e+10 6920 0.802
3.2e+10 5400 1.001
1.0e+11 4140 1.157
3.2e+11 3100 1.264
1.0e+12 2260 1.325
3.2e+12 1600 1.353
1.0e+13 1080 1.357
As central density rises, the electrons turn relativistic, the star
shrinks, and its mass climbs toward -- but never past -- 1.44 M_sun.
Beyond it there is no stable white dwarf: it collapses (type-Ia SN).
wrote C:\Users\acwic\symplectic-nbody\examples\output\chandrasekhar.svgStellar structure (Lane-Emden)
A self-gravitating polytropic gas sphere obeys the Lane-Emden equation. Its density profile and surface radius depend on the index n: n=1 is the exact sin(xi)/xi, n=3 the Eddington standard model, n=5 has finite mass but infinite radius.
Lane-Emden stellar structure: polytrope index n
n surface xi_1 mass -xi1^2 theta meaning
--------------------------------------------------------------------
0.0 2.449 4.899 uniform-density sphere
1.0 3.142 3.142 analytic sin(xi)/xi
1.5 3.654 2.714 convective / white dwarf
3.0 6.897 2.018 Eddington standard model
5.0 inf n/a infinite radius
As n rises the star grows more centrally concentrated. n=1 is the
exact sin(xi)/xi; n=3 is the Eddington standard model of a star;
n=5 has finite mass but infinite radius.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lane_emden.svgThe main sequence & the HR diagram
Mass rules a star's life: L ~ M^3.5, so massive blue stars are millions of times brighter but burn out in a few Myr, while red dwarfs live hundreds of Gyr. Luminosity vs temperature traces the main sequence -- the backbone of the Hertzsprung-Russell diagram.
The main sequence: mass sets luminosity, temperature, and lifetime
mass (M_sun) L (L_sun) T_eff (K) lifetime
-------------------------------------------------
0.3 0.0 3258 202.9 Gyr
0.5 0.1 4153 56.6 Gyr
1.0 1.0 5772 10.0 Gyr
2.0 11.3 8023 1.8 Gyr
5.0 279.5 12398 179 Myr
10.0 3162.3 17232 32 Myr
30.0 147885.1 29037 2 Myr
L ~ M^3.5, so massive O/B stars are millions of times brighter but
burn out in a few Myr, while red dwarfs sip fuel for hundreds of Gyr.
Plotting L vs T gives the main sequence -- the backbone of the HR diagram.
wrote C:\Users\acwic\symplectic-nbody\examples\output\main_sequence.svgKelvin-Helmholtz time: why gravity can't power the Sun
Gravitational contraction (t_KH = G M^2 / R L) could light the Sun for only ~30 Myr -- far short of Earth's 4.5 Gyr age, the historic proof that stars need nuclear fusion. It is instead how long a protostar contracts before fusion ignites.
Kelvin-Helmholtz (gravity) vs nuclear timescales
Sun's KH time: 31 Myr -- vs Earth's 4500 Myr age.
Gravity alone runs the Sun for only ~30 Myr, so it MUST fuse hydrogen.
mass (M_sun) t_KH (Myr) t_nuclear (Myr)
--------------------------------------------
0.5 154.71 56568.5
1.0 31.42 10000.0
2.0 6.38 1767.8
5.0 0.78 178.9
10.0 0.16 31.6
The nuclear lifetime dwarfs the KH time at every mass -- fusion, not
contraction, powers stars. The KH time is instead how long a
protostar contracts before fusion ignites.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kelvin_helmholtz.svgCosmic distances & the discovery of acceleration
Every cosmological distance is one integral of 1/E(z). A dark-energy universe puts a given redshift farther away, so distant type-Ia supernovae look ~0.4 mag fainter -- the 1998 acceleration result. The angular-diameter distance also turns over near z~1.6.
Cosmic distances: the Hubble diagram and cosmic acceleration
z mu LCDM mu EdS Delta mu (fainter)
---------------------------------------------------
0.2 39.956 39.760 +0.196
0.5 42.261 41.862 +0.399
1.0 44.100 43.502 +0.598
1.5 45.189 44.480 +0.709
Type-Ia supernovae at z~0.5 sit ~0.4 mag ABOVE the decelerating
prediction -- fainter, hence farther, hence the expansion is
accelerating. That is the 1998 result (2011 Nobel Prize).
angular-diameter distance peaks at z = 1.61
(past it, more distant objects look angularly LARGER -- why the CMB
acoustic spots subtend about a degree).
wrote C:\Users\acwic\symplectic-nbody\examples\output\distances.svgExpansion of the universe (Friedmann)
The scale factor a(t) under the Friedmann equation: radiation gives a ~ t^1/2, matter a ~ t^2/3, dark energy exponential growth. A flat LCDM universe ages to ~0.96/H0 (~13.5 Gyr) and is now entering its accelerating dark-energy era.
Friedmann cosmology: the expansion of the universe (time in 1/H0)
age of a flat LCDM universe : 0.964/H0
(with H0 = 70 km/s/Mpc this is 13.5 Gyr)
universe expansion exponent a ~ t^n
------------------------------------------
radiation n ~ 0.50
matter n ~ 0.67
dark energy exponential (accelerating)
flat LCDM n ~ 1.14
Radiation gives t^1/2, matter t^2/3, dark energy exponential growth.
Our universe (LCDM) coasted through matter domination and is now
entering the accelerating dark-energy era.
wrote C:\Users\acwic\symplectic-nbody\examples\output\friedmann.svgCosmic recombination (Saha equation)
The universe went neutral -- releasing the cosmic microwave background -- at z~1400, T~3700 K, NOT at kT = 13.6 eV (~158000 K). The ~1.6 billion photons per baryon keep hydrogen ionized far below its binding energy; the Saha equation pins the transition.
Cosmic recombination: when the universe went neutral
naive guess (kT = 13.6 eV) : 157821 K
actual recombination (x = 0.5) : z = 1379, T = 3760 K
~40x cooler than the naive value, because there are ~1.6 billion
photons per baryon -- the hot tail keeps hydrogen ionized.
redshift temp (K) ionized x
----------------------------------
1600 4363 0.9925
1400 3818 0.6037
1300 3545 0.1867
1200 3273 0.0339
1100 3000 0.0041
1000 2728 0.0003
Below z~1100 the universe is neutral and transparent: the photons
free-stream to us as the cosmic microwave background.
wrote C:\Users\acwic\symplectic-nbody\examples\output\saha.svgCMB acoustic scale: the 1-degree spots
The sound horizon at recombination is a fixed ruler; seen across the distance to last scattering it subtends ~1 degree, putting the first acoustic peak at multipole l ~ 220. Its position pins the universe's geometry to flat.
The CMB acoustic scale (flat LCDM) recombination redshift : z = 1379 sound horizon r_s : 192 Mpc distance to last scattering: 13734 Mpc acoustic angle theta : 0.80 deg first acoustic peak : l ~ 225 (WMAP/Planck: 220) A fixed sound-horizon ruler at the edge of the observable universe subtends about a degree; the harmonics of that standing wave are the acoustic peaks, and their position pins the geometry to flat. wrote C:\Users\acwic\symplectic-nbody\examples\output\cmb.svg
Blackbody radiation: Planck, Wien, Stefan-Boltzmann
The universal thermal spectrum: hotter bodies peak bluer (lambda_max T = 2.9 mm K) and radiate as T^4. It sets stellar colors, the Sun's 500 nm peak, and the 2.725 K CMB's microwave peak.
Blackbody radiation: Wien's peak and Stefan-Boltzmann object T (K) peak flux (W/m^2) ------------------------------------------------------------ CMB 3 1.06 mm 3.13e-06 human body 310 9348 nm 5.24e+02 cool star (M) 3000 966 nm 4.59e+06 Sun (G) 5772 502 nm 6.29e+07 hot star (B) 20000 145 nm 9.07e+09 Hotter bodies peak bluer (Wien) and radiate vastly more (Stefan- Boltzmann T^4): a 20000 K B-star outshines the Sun per unit area by 144x. The 2.725 K CMB peaks in the microwave. wrote C:\Users\acwic\symplectic-nbody\examples\output\blackbody.svg
Compton & inverse-Compton scattering
Photons trade energy with electrons: Compton down-shifts a photon (shift = lambda_C(1-cos theta), lambda_C = 2.426 pm), while inverse Compton off a relativistic electron boosts it by ~gamma^2 -- turning CMB and starlight into X-rays and gamma-rays.
Compton scattering (photon off a stationary electron)
Compton wavelength : 2.426 pm
electron rest energy : 511 keV
angle (deg) shift (pm) E scattered (keV)
--------------------------------------------
0 0.000 500.0
45 0.711 388.6
90 2.426 252.7
135 4.142 187.2
180 4.853 169.1
Inverse Compton (fast electron kicks a photon UP):
gamma= 10: boost x132 (a 1 meV CMB photon -> 0 eV)
gamma= 100: boost x13332 (a 1 meV CMB photon -> 13 eV)
gamma= 1000: boost x1333332 (a 1 meV CMB photon -> 1333 eV)
Compton down-shifts photons off electrons; inverse Compton up-shifts
them off relativistic electrons -- how CMB and starlight become X-rays.
wrote C:\Users\acwic\symplectic-nbody\examples\output\compton.svgThe Larmor formula: radiation from acceleration
Any accelerating charge radiates, with power P ~ q^2 a^2. Relativistically a circular accelerator boosts it by gamma^4 and a linear one by gamma^6. It also dooms the classical atom (an electron spirals in in ~1.6e-11 s) -- and it's the engine under synchrotron.
The Larmor formula: accelerating charges radiate (P ~ a^2)
non-relativistic power at a=1e+20 m/s^2: 5.71e-14 W
gamma perp (gamma^4) parallel (gamma^6)
----------------------------------------------
1 5.71e-14 5.71e-14
10 5.71e-10 5.71e-08
100 5.71e-06 5.71e-02
1000 5.71e-02 5.71e+04
classical hydrogen atom collapse time: 1.55e-11 s
-- an orbiting electron radiating by Larmor spirals in almost instantly.
That catastrophe is what quantum mechanics had to fix. The same formula,
boosted by gamma^4, is the engine of synchrotron radiation.
wrote C:\Users\acwic\symplectic-nbody\examples\output\larmor.svgSynchrotron radiation: cosmic radio glow
Relativistic electrons spiralling in magnetic fields radiate at a critical frequency ~ gamma^2 B (GHz radio for gamma~1e4 in microgauss fields). A power-law electron distribution N(E)~E^-p gives a power-law spectrum with index (p-1)/2 -- how we read jets and supernova remnants.
Synchrotron radiation (B = 1 nT)
gamma nu_c (Hz) P (W) cooling
------------------------------------------------
1e+02 4.20e+05 1.06e-28 2.5e+09 yr
1e+03 4.20e+07 1.06e-26 2.5e+08 yr
1e+04 4.20e+09 1.06e-24 2.5e+07 yr
1e+05 4.20e+11 1.06e-22 2.5e+06 yr
1e+06 4.20e+13 1.06e-20 2.5e+05 yr
spectral index alpha = (p-1)/2 of a power-law electron population:
p=2.0: alpha=0.50 (S(nu) ~ nu^-0.50)
p=2.5: alpha=0.75 (S(nu) ~ nu^-0.75)
p=3.0: alpha=1.00 (S(nu) ~ nu^-1.00)
Relativistic electrons in weak cosmic fields shine in the radio; the
spectral slope reveals the electron energy distribution -- how we read
jets, radio galaxies, and supernova remnants.
wrote C:\Users\acwic\symplectic-nbody\examples\output\synchrotron.svgOptical depth: where a star's surface is
Light is attenuated as exp(-tau) crossing matter. A star has no solid surface -- its photosphere is the layer where the inward optical depth reaches tau ~ 2/3, the depth photons escape from and that sets the effective temperature.
Optical depth: how far light sees into matter
tau transmitted regime
--------------------------------------
0.10 0.905 thin
0.50 0.607 thin
0.67 0.513thin (photosphere)
1.00 0.368 thin
3.00 0.050 thick
10.00 0.000 thick
at n_e = 1e+20 /m^3 (Thomson): mean free path 150319 km,
photosphere depth 100213 km.
A star has no solid surface -- its photosphere is simply the layer
where the inward optical depth reaches ~2/3, the depth from which
photons finally escape and set the effective temperature.
wrote C:\Users\acwic\symplectic-nbody\examples\output\optical_depth.svgBig Bang nucleosynthesis: the primordial 25% helium
In the first minutes the neutron/proton ratio freezes at ~1/6, decays to ~1/7, and nearly all surviving neutrons lock into helium-4, giving Y_p ~ 0.25. That quarter-helium, seen everywhere, is a triumph of the hot Big Bang.
Big Bang nucleosynthesis: the origin of primordial helium stage n/p ------------------------------------------ equilibrium at 10 MeV (t~0.01 s) 0.879 freeze-out at 0.8 MeV (t~1 s) 0.199 after neutron decay (t~200 s) 0.152 primordial helium mass fraction Y_p = 0.264 (observed ~0.245-0.25 -- a triumph of the hot Big Bang) Neutrons and protons start nearly equal, the ratio freezes at ~1/6 as the weak interaction shuts off, decays to ~1/7, and nearly all surviving neutrons end up in helium-4: about a quarter of all mass. wrote C:\Users\acwic\symplectic-nbody\examples\output\bbn.svg
The Eddington luminosity & black-hole growth
Radiation pressure caps how bright -- and how fast-growing -- an accreting object can be: L_Edd = 4 pi G M m_p c / sigma_T, linear in mass. The e-folding growth time is ~45 Myr, so building a billion-solar-mass quasar from a seed takes ~0.8 Gyr.
The Eddington luminosity: the brightness limit of accretion Salpeter e-folding time: 45.0 Myr (eta=0.1, Eddington-limited) object M (Msun) L_Edd (L_sun) Mdot (Msun/yr) ------------------------------------------------------------------ Sun 1e+00 3.28e+04 2.22e-08 stellar BH 1e+01 3.28e+05 2.22e-07 Sgr A* 4e+06 1.31e+11 8.88e-02 quasar 1e+09 3.28e+13 2.22e+01 L_Edd is linear in mass and radius-independent. Above it, radiation pressure blows the accreting gas away, so it caps how fast a black hole can grow -- the exponential Salpeter track below. wrote C:\Users\acwic\symplectic-nbody\examples\output\eddington.svg
Bondi accretion: feeding on ambient gas
Spherical accretion onto a compact object: Mdot ~ M^2 rho / c_s^3. It runs away with mass and is far stronger in cold gas -- a black hole in a molecular cloud eats millions of times faster than one in hot coronal gas. Compared to Eddington, it says whether growth is supply- or radiation-limited.
Bondi accretion (10 M_sun object, n ~ 1 /cm^3)
gas T (K) c_s (km/s) r_B (AU) Mdot (Msun/yr)
------------------------------------------------
1e+02 1.2 6450.0 9.09e-11
1e+03 3.7 645.0 2.87e-12
1e+04 11.7 64.5 9.09e-14
1e+06 117.3 0.6 9.09e-17
1e+07 370.9 0.1 2.87e-18
Rate ~ M^2 rho / c_s^3, so accretion runs away with mass and is far
stronger in cold gas: a black hole in a 100 K molecular cloud accretes
millions of times faster than one in hot 10^7 K coronal gas.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bondi.svgHawking radiation & black-hole thermodynamics
Quantum effects at the horizon give a black hole a temperature T ~ 1/M and an entropy = 1/4 its area in Planck units. Big holes are colder and live longer (t_evap ~ M^3); a ~1.7e11 kg primordial hole evaporates in a Hubble time, while a solar-mass one is ~60 nK and eternal.
Hawking radiation: black holes are not black
primordial mass evaporating in a Hubble time: 1.73e+11 kg
(about the mass of a large asteroid, in a horizon ~1e-16 m across)
object T (K) t_evap (yr) S/k_B
------------------------------------------------------------
primordial (now) 7.09e+11 1.38e+10 7.94e+38
1 solar mass 6.17e-08 2.10e+67 1.05e+77
Sgr A* (4e6 Msun) 1.54e-14 1.34e+87 1.68e+90
M87* (6.5e9 Msun) 9.49e-18 5.76e+96 4.43e+96
Bigger holes are COLDER and live longer (T ~ 1/M, t_evap ~ M^3).
A stellar black hole is nanokelvin-cold and effectively eternal;
its entropy exceeds that of everything else in the observable universe.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hawking.svgKerr black holes: spin & frame-dragging
A rotating black hole drags spacetime around it. Its horizon shrinks with spin, an ergosphere appears outside it, and the ISCO splits: prograde orbits reach down toward 1M at extremal spin while retrograde ones recede to 9M -- how black-hole spins are measured.
Kerr black hole: spin sets the horizon and the ISCO (units of M)
a/M horizon ISCO pro ISCO retro Omega_H
-------------------------------------------------
0.00 2.000 6.000 6.000 0.000
0.30 1.954 4.979 6.949 0.077
0.60 1.800 3.829 7.851 0.167
0.90 1.436 2.321 8.717 0.313
0.99 1.141 1.454 8.972 0.434
1.00 1.000 1.000 9.000 0.500
A fast-spinning hole drags its prograde ISCO down toward the horizon
(1M at extremal spin), so its accretion disk reaches deeper and radiates
more efficiently. Measuring the disk's inner edge is how spins are found.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kerr.svgPenrose process: mining spin energy
Inside the ergosphere a fragment can carry negative energy, so the escaping piece leaves with more than it entered -- energy mined from the hole's spin. Up to 29% of an extremal hole's mass-energy is extractable, and removing it only grows the horizon area (area theorem).
The Penrose process: extracting a black hole's rotational energy
maximum extractable fraction (extremal a=M): 29.3% of M c^2
a/M M_irr/M E_rot fraction horizon area
----------------------------------------------
0.00 1.0000 0.0% 50.27
0.30 0.9884 1.2% 49.11
0.60 0.9487 5.1% 45.24
0.90 0.8473 15.3% 36.09
0.99 0.7553 24.5% 28.68
1.00 0.7071 29.3% 25.13
Up to ~29% of an extremal hole's mass-energy can be mined from its
spin. Doing so raises the irreducible mass and horizon area -- the
area never shrinks (Hawking), which is the second law for black holes.
wrote C:\Users\acwic\symplectic-nbody\examples\output\penrose.svgSchwarzschild black-hole orbits
Strong-field GR from the effective potential V=(1-2M/r)(1+L^2/r^2): the ISCO at 6M, the photon sphere at 3M, bound orbits that precess tens of degrees per orbit, and low-angular-momentum orbits that plunge through the horizon.
Orbits around a Schwarzschild black hole (units of M)
event horizon : r = 2 M
photon sphere : r = 3 M
ISCO : r = 6 M
bound orbit (r: 10-20 M): perihelion advance 126.2 deg/orbit
(Mercury's is 43 arcsec/CENTURY; here it's tens of degrees PER ORBIT)
plunging orbit: falls from r=12 M through the horizon to r=0.01 M
wrote C:\Users\acwic\symplectic-nbody\examples\output\schwarzschild.svg
The bound orbit is a precessing rosette; the plunger spirals through
the horizon. Both are exact Schwarzschild geodesics.Gravitational time: redshift, GPS & Shapiro delay
Clocks run slower deeper in gravity and light lags near mass. Pound-Rebka measured the redshift on a tower; GPS satellites gain ~38 us/day (correct it or navigation fails); the Shapiro radar delay past the Sun is the tightest Solar-System test of GR.
Gravitational time effects (classic GR tests) Pound-Rebka (22.5 m tower) : z = 2.45e-15 (measured 2.5e-15) GPS clock gain : +38.5 us/day (must be corrected or GPS drifts km/day) Sun surface redshift : z = 2.12e-06 (2.12e-6) Shapiro delay (past the Sun): 281 us (Cassini ~240-280 us) Clocks run slower deeper in gravity and light lags near mass. The GPS correction is relativity you rely on daily; the Shapiro delay is the tightest Solar-System test of general relativity. wrote C:\Users\acwic\symplectic-nbody\examples\output\gr_time.svg
Frame-dragging & geodetic precession (Gravity Probe B)
An orbiting gyroscope precesses two ways: geodetic (from spatial curvature, ~6600 mas/yr) and frame-dragging (from Earth's rotation twisting spacetime, ~40 mas/yr). Gravity Probe B measured both -- the frame-dragging term is ~180x smaller and took near-perfect gyros to see.
Gravity Probe B: two relativistic gyroscope precessions effect predicted measured ---------------------------------------------------- geodetic (de Sitter) 6638 mas/yr 6602 frame-dragging (LT) 41.1 mas/yr 37.2 Geodetic precession comes from the curvature of space the gyro is carried through; frame-dragging comes from Earth's rotation twisting spacetime. The latter is ~180x smaller -- it took a dedicated experiment with near-perfect gyroscopes to measure it. wrote C:\Users\acwic\symplectic-nbody\examples\output\lense_thirring.svg
Galaxy rotation curves & dark matter
A visible disk alone gives a Keplerian decline (v ~ r^-1/2) past its edge; real galaxies stay flat. Adding an NFW dark halo, whose enclosed mass keeps growing as ~r, flattens the curve -- the classic evidence for dark matter.
Galaxy rotation curves: the case for dark matter visible disk only : outer slope -0.490 (Keplerian decline = -0.50) disk + dark halo : outer slope -0.000 (flat = 0.00) visible-only v(r): .::--------------------::::::::::::::::::::::::::::::::..... disk+halo v(r): :-=+++*****################################@################ The visible curve falls off; the observed curve is flat. The gap is the dark-matter halo, whose enclosed mass keeps growing as ~ r. wrote C:\Users\acwic\symplectic-nbody\examples\output\rotation_curve.svg
Faber-Jackson: elliptical galaxy scaling
Elliptical galaxies obey L ~ sigma^4 -- luminosity from the random stellar velocity dispersion, following from the virial theorem plus a roughly constant mass-to-light ratio. A line width gives the luminosity, hence the distance -- the elliptical twin of Tully-Fisher.
The Faber-Jackson relation: L ~ sigma^4 for ellipticals
sigma (km/s) L (L_sun) virial M (M_sun)
--------------------------------------------
50 7.81e+07 2.91e+09
100 1.25e+09 1.16e+10
200 2.00e+10 4.65e+10
300 1.01e+11 1.05e+11
400 3.20e+11 1.86e+11
Luminosity climbs as the fourth power of the velocity dispersion, so
a spectral line width (sigma) pins a galaxy's luminosity -- and,
against its apparent brightness, its distance. It is the elliptical-
galaxy twin of the Tully-Fisher relation for spirals.
wrote C:\Users\acwic\symplectic-nbody\examples\output\faber_jackson.svgMOND: flat curves without dark matter
The rival to the dark halo: instead of adding unseen mass, MOND modifies gravity below a0 ~ 1.2e-10 m/s^2. A bare baryonic mass then has a naturally flat rotation curve and obeys the tight baryonic Tully-Fisher law v^4 = G M a0 -- MOND's sharpest prediction.
MOND: modified gravity instead of dark matter
a0 = 1.20e-10 m/s^2 baryonic mass = 6e10 M_sun
asymptotic flat speed v = (G M a0)^1/4 = 175.8 km/s
r (kpc) Newton (km/s) MOND (km/s)
--------------------------------------
2 359.2 368.9
5 227.2 257.1
10 160.7 215.4
20 113.6 195.0
40 80.3 185.2
80 56.8 180.5
Newtonian gravity on the visible mass alone falls off (Keplerian);
MOND flattens the curve with no dark matter. Its sharpest prediction is
the baryonic Tully-Fisher law v^4 = G M a0, tight across real galaxies.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mond.svgGravitational lensing
Mass bends light by 4GM/(c^2 b) -- twice the Newtonian value, the 1919 eclipse result (1.75 arcsec at the Sun's limb). A point-mass lens splits a source into two images, an Einstein ring at perfect alignment, and the symmetric microlensing brightening used to find exoplanets.
Gravitational lensing
light deflection at the Sun's limb : 1.751 arcsec (Eddington 1919 measured ~1.75)
microlensing light curve (min impact u0=0.2):
............:::::::----====++***####@@####***++====----:::::::............
peak magnification A_max = 5.07 at closest approach
A point-source drifting behind a mass brightens symmetrically then
fades -- the achromatic, time-symmetric microlensing signature used
to find exoplanets and dark compact objects.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lensing.svgTisserand parameter & gravity assists
Across a planetary flyby a small body's semi-major axis and eccentricity change a lot, but the Tisserand parameter T = a_p/a + 2 sqrt(a/a_p (1-e^2)) cos i barely moves -- how Tisserand recognized comets Jupiter had reshaped, and what bounds a gravity assist.
Tisserand parameter across a gravity assist
before after change
----------------------------------------
a (AU) 1.281 0.890 -0.391
e 0.330 0.265 -0.065
T_planet 2.9404 2.9432 +0.00277
a and e are reshaped by the flyby; the Tisserand parameter is nearly
unchanged. That is how Tisserand recognized returning comets whose orbits
Jupiter had scrambled -- and it bounds what one flyby can do.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tisserand.svgCoorbital orbits: tadpoles & horseshoes
A body sharing a planet's orbit librates in the rotating frame: a tadpole loops one Lagrange point (Jupiter's Trojans); a horseshoe wraps around L3 enclosing both L4 and L5, turning back before it reaches the planet (Saturn's Janus & Epimetheus, Earth's Cruithne).
Coorbital motion in the CR3BP rotating frame (mu = 0.001) tadpole (near L4): angular range 78 deg -> tadpole horseshoe (near L3): angular range 315 deg -> horseshoe The tadpole loops one Lagrange point (like Jupiter's Trojans); the horseshoe wraps around L3 enclosing both L4 and L5, turning back before it reaches the planet (like Saturn's moons Janus & Epimetheus). wrote C:\Users\acwic\symplectic-nbody\examples\output\coorbital.svg
Mean-motion resonance
Two planets at the 2:1 spacing lock into resonance: the resonant argument phi librates in a bounded band instead of circulating. This is what carves the Kirkwood gaps and binds the Laplace resonance of Io-Europa-Ganymede.
Mean-motion resonance: 2:1 resonant argument phi resonant (2:1 spacing) : phi range 2.10 rad -> LIBRATES (locked) off-resonance : phi range 6.27 rad -> circulates (2pi=6.28) resonant phi : ====++=====-----:-----======+====---::-----===+======------::-----====== off-res phi : -.#+-.*=: *=:#+-.*=: *=:#+-.*+: *=: +-.#=: *=:#+-.#+: *=: +=.#+- *=: *-. A librating resonant argument IS the lock: the pair's periods stay commensurate. This carves the Kirkwood gaps and builds the Laplace resonance of Io-Europa-Ganymede. wrote C:\Users\acwic\symplectic-nbody\examples\output\resonance.svg
Kozai-Lidov cycles
In a hierarchical triple the inner orbit trades eccentricity for inclination and back, conserving sqrt(1-e^2) cos i. Above a critical inclination (~39.2 deg) the eccentricity is driven to large values -- the mechanism behind hot-Jupiter migration and compact-binary mergers.
Kozai-Lidov cycles in a hierarchical triple critical inclination : 39.23 deg start : e=0.01, i=80 deg e_max measured / analytic : 0.975 / 0.975 inclination swings : 39.3 - 80.0 deg eccentricity : # # :+*=. =: # # -: :#. .:---:. =#. -= -- # * * # -- +: -*. :#=. .:=+ inclination : @ #=####*#######-#.#####=##############+########:#*#*#:########*###-######## e peaks exactly when i dips: they trade the conserved Theta = sqrt(1-e^2) cos i. This drives hot-Jupiter migration and compact-binary mergers. wrote C:\Users\acwic\symplectic-nbody\examples\output\kozai.svg
Roche limit & tidal disruption
A rubble-pile satellite survives only outside the Roche limit; inside it, the tide overwhelms self-gravity and tears it into a stream -- how planetary rings and Shoemaker-Levy 9's fragment chain formed. The surviving bound fraction drops sharply across the limit.
Roche limit: tidal disruption of a rubble-pile satellite
d / d_Roche surviving bound fraction
------------------------------------------------
0.40 ------------------------------ 0.00
0.60 #################------------- 0.57
0.80 ##########################---- 0.88
1.00 ###########################--- 0.90
1.25 ############################-- 0.95
1.50 #############################- 0.98
2.00 ############################## 1.00
3.00 ############################## 1.00
Below ~1 Roche the satellite is shredded; above it, it survives.
wrote C:\Users\acwic\symplectic-nbody\examples\output\roche_disruption.svg
The rubble pile stretches into a tidal stream -- how Saturn's rings
and Shoemaker-Levy 9's fragment chain came to be.Tidal heating: why Io erupts
A moon on an eccentric orbit is flexed by the varying tide and dissipates the energy as heat: dE/dt ~ (k2/Q) e^2 R^5 / a^{15/2}. For Io this is ~1e14 W (40x Earth's heat flux), and the eccentricity is forced by the Laplace resonance -- resonance and volcanoes linked.
Tidal heating of the Galilean moons (k2/Q ~ 0.015) moon power (W) flux (W/m^2) ------------------------------------ Io 9.33e+13 2.238 Europa 6.37e+12 0.208 Ganymede 5.48e+10 0.001 Callisto 1.65e+10 0.000 Io: 9.3e+13 W ~ 40x Earth's internal heat flux, which is why it is the most volcanic body known. The heating goes as e^2, and Io's eccentricity is forced by the Laplace 4:2:1 resonance -- no resonance, no eccentricity, no volcanoes. wrote C:\Users\acwic\symplectic-nbody\examples\output\tidal_heating.svg
Roche lobes: binary mass transfer
Each star in a binary owns a Roche lobe meeting its companion's at L1. When a star fills its lobe, gas pours through L1 onto the companion. From a lighter donor the orbit widens (stable transfer); from a heavier one it runs away -- the physics of X-ray binaries and type-Ia progenitors.
Roche lobes and binary mass transfer
q = M_donor/M_acc R_L/a d ln a/d ln M transfer
-------------------------------------------------------
0.2 0.252 -1.6 stable
0.5 0.321 -1.0 stable
1.0 0.379 0.0 unstable
2.0 0.440 2.0 unstable
5.0 0.521 8.0 unstable
When a star fills its Roche lobe, gas pours through L1 onto its
companion. From a lighter donor the orbit widens and transfer is stable
(cataclysmic variables, X-ray binaries); from a heavier donor it runs
away -- the path to mergers and type-Ia supernovae.
wrote C:\Users\acwic\symplectic-nbody\examples\output\roche_lobe.svgVirial theorem & violent relaxation
For a bound gravitational system 2T + U = 0. A Plummer sphere sits at 2T/U = -1; a cold, sub-virial cluster collapses, overshoots, and relaxes to the same value -- forgetting its initial state through violent relaxation.
Virial theorem: a bound gravitational system settles at 2T/U = -1 equilibrium Plummer : running <2T/U> -> -0.992 (target -1) cold cluster : start -0.083 -> running <2T/U> -0.952 cold-cluster instantaneous 2T/U (collapse & relaxation): #*:...::.:.::::::---::::::::.:::::::.:::::::::::::::::::::::::::::::::::::::::::.::-:::::::::::::.:: wrote C:\Users\acwic\symplectic-nbody\examples\output\virial.svg Both running averages converge on -1: the cold cluster forgets its cold start through violent relaxation and lands in virial balance.
Galaxy clusters: virial temperature & X-rays
The virial theorem applied to a cluster's gas: falling into a 10^15-solar-mass well heats it to a few keV (~10^8 K), radiating X-rays. Because kT ~ M^{2/3}, an X-ray temperature weighs the cluster's total (mostly dark) mass.
Galaxy clusters: virial temperature and the M-T relation
mass (M_sun) radius (Mpc) kT (keV) T (K)
--------------------------------------------------
1e+13 0.43 0.31 3.6e+06
3e+13 0.63 0.67 7.8e+06
1e+14 0.93 1.45 1.7e+07
3e+14 1.36 3.13 3.6e+07
1e+15 2.00 6.74 7.8e+07
A massive cluster reaches several keV (~10^8 K), radiating thermal
bremsstrahlung X-rays. Because kT ~ M^{2/3}, an X-ray temperature
measures the cluster's total (mostly dark) mass -- how clusters are weighed.
wrote C:\Users\acwic\symplectic-nbody\examples\output\cluster.svgSunyaev-Zeldovich effect: clusters shadowing the CMB
The same hot cluster gas inverse-Compton scatters CMB photons, imprinting a Compton-y distortion: a cold spot dT/T = -2y in the radio. It is redshift-independent, so SZ surveys find clusters clear across the universe.
The Sunyaev-Zeldovich effect: hot clusters distorting the CMB cluster n_e (/m^3) kT (keV) y dT (uK) -------------------------------------------------------------- group 3e+02 2.0 1.21e-06 -6.6 Coma-like 1e+03 8.0 3.21e-05 -175.1 massive 3e+03 12.0 2.89e-04 -1576.3 In the Rayleigh-Jeans band a cluster is a COLD spot: dT/T = -2y. redshift-independent? True -- the SZ signal does not dim with distance, so SZ surveys find clusters clear across the universe. wrote C:\Users\acwic\symplectic-nbody\examples\output\sz.svg
Bremsstrahlung: the X-rays of cluster gas
Free electrons braking in ion fields radiate free-free X-rays with emissivity ~ n^2 sqrt(T). The cooling time ~ sqrt(T)/n drops below a Hubble time in dense cluster cores (cooling flows) but never in the outskirts -- the emission whose CMB imprint is the SZ effect.
Thermal bremsstrahlung of cluster gas (T = 5e+07 K)
n_e (/m^3) emissivity (W/m^3) t_cool (Gyr) flow?
------------------------------------------------------
1e+02 9.90e-33 662.9 no
1e+03 9.90e-31 66.3 no
1e+04 9.90e-29 6.6 yes
1e+05 9.90e-27 0.7 yes
Emissivity goes as n^2, so dense cores glow brightest and cool fastest
(t_cool ~ sqrt(T)/n). A cooling flow develops where t_cool drops below
the Hubble time; the tenuous outskirts effectively never cool. This is
the X-ray emission whose CMB imprint is the SZ effect.
wrote C:\Users\acwic\symplectic-nbody\examples\output/bremsstrahlung.svgPair production & the gamma-ray horizon
Two photons colliding turn into an electron-positron pair once E1 E2 (1 - cos theta) >= 2(m_e c^2)^2, so two 511 keV gammas just pair head-on. A single high-energy gamma pair-produces off a soft background photon above (m_e c^2)^2 / E_bg -- TeV gammas from blazars are eaten by starlight/IR, PeV gammas by the CMB. The universe is opaque to gamma rays beyond a horizon that shrinks as energy rises.
Photon-photon pair production: gamma + gamma -> e+ e-
electron rest energy: 511 keV
head-on threshold (each photon): 511 keV
background photon E (eV) threshold gamma
------------------------------------------------
CMB 6.0e-04 435 TeV
infrared (EBL) 1.0e-01 3 TeV
optical (EBL) 2.0e+00 131 GeV
X-ray 1.0e+03 261 MeV
A gamma ray is absorbed once its energy exceeds (m_e c^2)^2 / E_bg,
so TeV photons from distant blazars are eaten by starlight/IR and
PeV photons by the CMB -- the universe has a gamma-ray horizon that
shrinks as the photon energy rises.
wrote C:\Users\acwic\symplectic-nbody\examples\output\pair_production.svgPrecession of the equinoxes
Earth's equatorial bulge, tilted 23.4 deg to the ecliptic, feels an uneven Sun+Moon pull that torques the spin axis into a 26,000-year cone -- like a leaning gyroscope. Torque scales as M/r^3, so the nearby Moon beats the Sun ~2.2 to 1; the sum is ~50.3 arcsec/yr. That is why Polaris is only a temporary North Star and why zodiac dates have slipped a whole sign since antiquity.
Precession of the equinoxes (luni-solar torque on Earth's bulge)
source arcsec/yr period (yr)
----------------------------------------
Sun 15.95 81269
Moon 34.70 37352
Sun+Moon 50.64 25591
Moon/Sun torque ratio: 2.18 (nearby body wins: torque ~ M / r^3)
obliquity: 23.44 deg
measured luni-solar precession: 50.29 arcsec/yr, period ~25772 yr
The pole traces a 47-deg-wide circle on the sky, so Polaris is only
our temporary North Star -- Vega held the title ~12000 BC and will
again ~14000 AD. The same drift slips the equinox one zodiac sign
every ~2150 years (the astrological 'ages').
wrote C:\Users\acwic\symplectic-nbody\examples\output\axial_precession.svgAlfven waves & the magnetized solar wind
A magnetic field threading a plasma behaves like a set of tensioned strings: pluck the field lines and they spring back at the Alfven speed v_A = B / sqrt(mu0 rho). The plasma beta = p_gas/p_mag says who is in charge -- beta << 1 in the field-dominated corona, beta > 1 in gas-dominated interiors. The solar wind starts sub-Alfvenic (the Sun's field co-rotates and brakes it), then crosses the Alfven surface near ~15 R_sun and coasts out decoupled from the Sun's spin.
Alfven waves in magnetized plasma: v_A = B / sqrt(mu0 rho)
environment B (T) n (/m^3) v_A (km/s) beta
------------------------------------------------------------------
active corona 1.0e-02 1.0e+15 6897.6 0.0007
quiet corona 1.0e-03 1.0e+14 2181.2 0.0035
solar wind 1 AU 5.0e-09 5.0e+06 48.8 0.6940
warm ISM 5.0e-10 1.0e+06 10.9 1.1104
beta < 1 (corona): magnetic tension rules -- the field channels the
plasma and stores the energy that heats the corona and drives flares.
beta > 1 (dense interiors): gas pressure drags the field around.
The solar wind starts sub-Alfvenic (Sun's field co-rotates the plasma
and brakes the spin), then crosses the Alfven surface near ~10-20 R_sun
and coasts out super-Alfvenic -- decoupled from the Sun's rotation
(Parker Solar Probe crossed the real surface near ~15-20 R_sun).
wrote C:\Users\acwic\symplectic-nbody\examples\output\alfven.svgThe Parker spiral
The solar wind drags the Sun's magnetic field radially outward while its footpoints stay rooted in a Sun that rotates every ~25 days -- a rotating sprinkler. Each parcel flies straight out, but the field line traces an Archimedean spiral: nearly radial near the Sun, bent ~45 deg at Earth (the garden-hose angle), nearly azimuthal by Jupiter. It is why western-limb flares connect best to Earth along the spiral.
The Parker spiral: the Sun's field wound up by its rotation
solar wind speed: 400 km/s, rotation period: 25.4 days
location r (AU) angle (deg) |B| (nT)
-----------------------------------------------
Mercury 0.39 22.7 35.63
Venus 0.72 37.7 12.18
Earth 1.00 47.0 7.33
Mars 1.52 58.5 4.14
Jupiter 5.20 79.8 1.05
Saturn 9.58 84.4 0.56
Near the Sun the field is nearly radial; by Earth it is bent ~45 deg
(the classic garden-hose angle); past Jupiter it is nearly azimuthal.
B_r falls as 1/r^2 but B_phi only as 1/r, so the distant heliospheric
field is mostly the wound-up azimuthal component. This is why western-
limb solar flares connect best to Earth along the spiral field line.
wrote C:\Users\acwic\symplectic-nbody\examples\output\parker_spiral.svgMagnetic braking & gyrochronology
That same magnetized wind is a brake. Plasma stays locked to the field out to the Alfven radius (~15 R_sun), so it corotates on a long lever arm and bleeds angular momentum -- fast rotators brake hardest, so stellar spins converge onto one sequence. Skumanich's law P ~ t^(1/2) then turns a measured rotation period into an age: the Sun's 25-day spin reads 4.6 Gyr, a 3-day Pleiad reads ~60 Myr.
Magnetic braking & gyrochronology: a star's spin is its clock
Skumanich law: P(t) = P_sun sqrt(t / t_sun), so t = t_sun (P/P_sun)^2
rotator P (days) gyro age (Gyr)
------------------------------------------------
Pleiades member 3.0 0.06
young field star 6.0 0.25
Hyades member 8.5 0.51
the Sun 25.4 4.57
old thick-disk star 35.0 8.67
solar-wind Alfven radius ~ 16 R_sun -- the long lever arm that
lets a feeble ~1e-14 Msun/yr mass loss brake the whole star. Fast
rotators brake hardest, so a broad spread of young spins converges
onto one age-period sequence -- which is what makes the clock work.
wrote C:\Users\acwic\symplectic-nbody\examples\output\magnetic_braking.svgTidal locking
Internal friction drags a body's tidal bulge out of line with its primary; the misaligned bulge feels a torque that despins it toward synchronous rotation. The locking time goes as a^6, so close-in moons lock in a geological blink (Phobos, Io) while distant ones never do -- the Moon locked to Earth long ago, but the Earth needs far longer than the universe is old to lock back to the Moon's weaker tide.
Tidal locking: t_lock ~ a^6 I Q / (G M_p^2 k2 R^5)
age of the solar system: 4.57 Gyr
body -> primary t_lock (Gyr) locked?
------------------------------------------------
Moon -> Earth 0.00933 yes
Phobos -> Mars 7.87e-11 yes
Io -> Jupiter 1.01e-07 yes
Earth -> Moon 17.5 no
Mercury -> Sun 0.349 yes
The a^6 dependence is everything: close-in moons lock in a geological
blink, while the Earth (braking only on the Moon's weak tide) needs far
longer than the universe is old. Mercury dodged full locking into a 3:2
spin-orbit resonance; hot Jupiters at a few stellar radii are all locked.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tidal_locking.svgJeans escape & the cosmic shoreline
At the exobase, molecules faster than escape speed leave for good. Light, hot gases have a fatter Maxwell-Boltzmann tail, so the escape parameter lambda = v_esc^2/v_th^2 decides who keeps an atmosphere: Earth holds N2/O2/CO2 but loses H2 and He, the hot low-gravity Moon holds almost nothing, cold Titan clings even to nitrogen, and Jupiter keeps everything. Escape speed vs temperature is a cosmic shoreline.
Jeans escape: lambda = v_esc^2 / v_th^2 = G M m / (R k_B T)
retention rule of thumb: keep a gas when v_esc >= 6 v_th (lambda >= 36)
body v_esc H2 He H2O N2 O2 CO2 (Y=kept, .=lost)
-------------------------------------------------------------------
Earth 11.2k . . Y Y Y Y
Moon 2.4k . . . . . Y
Mars 5.0k . . Y Y Y Y
Titan 2.6k . . Y Y Y Y
Jupiter 60.2k Y Y Y Y Y Y
Earth keeps N2/O2/CO2/water but loses H2 and He (why our air is heavy);
the hot, low-gravity Moon loses all but the heaviest (and even CO2 goes
to non-thermal escape); cold Titan clings even to N2;
giant Jupiter keeps everything, including hydrogen. This 'cosmic
shoreline' -- escape speed vs temperature -- sorts which worlds have air.
wrote C:\Users\acwic\symplectic-nbody\examples\output\jeans_escape.svgThe snow line
A protoplanetary disk cools with distance as T ~ r^(-1/2), so ice condenses only beyond the point where it drops past ~160 K -- the snow line, at ~3 AU for the young Sun. Inside, water is vapour and planets grow small and dry; outside, ice roughly triples the solid density and lets giant cores grow fast enough to grab gas. Water, CO2 and CO each have their own frost line, sorting the disk by composition.
The snow line: disk T(r) = (L / 16 pi sigma r^2)^(1/4) ~ r^(-1/2)
planet r (AU) T (K) state
--------------------------------------
Mercury 0.39 445.7 rock
Venus 0.72 328.0 rock
Earth 1.00 278.3 rock
Mars 1.52 225.8 rock
Jupiter 5.20 122.1 rock+ice
Saturn 9.58 89.9 rock+ice
Uranus 19.20 63.5 rock+ice
Neptune 30.10 50.7 rock+ice
water snow line: 3.03 AU (T = 160 K)
CO2 frost line: 15.8 AU
CO frost line: 194 AU (out past Neptune)
Inside ~3 AU water is vapour, so only rock and metal condense and the
terrestrial planets grew small and dry. Beyond it, ice roughly triples
the solid surface density, letting Jupiter's core grow fast enough to
grab nebular gas before the disk dissipated. The snow line is the
dividing line between the rocky inner and giant/icy outer solar system.
wrote C:\Users\acwic\symplectic-nbody\examples\output\snow_line.svgPoynting-Robertson drag
A dust grain re-radiates absorbed sunlight isotropically in its own frame, but aberration turns that into a faint forward headwind in the Sun's frame -- draining angular momentum so the grain spirals in. The inspiral time goes as r^2 and grain size, so micron grains at 1 AU fall in within a few thousand years; grains below the ~0.4 micron blow-out size are unbound and ejected. The zodiacal dust must be resupplied.
Poynting-Robertson drag: dust's own re-radiated light is a headwind
blow-out size (beta = 1/2): 0.38 micron (smaller grains are blown straight out)
grain size beta fate / t_PR from 1 AU
------------------------------------------------
0.1 um 1.914 blown out (unbound)
0.3 um 0.638 blown out (unbound)
0.5 um 0.383 1,046 yr
1 um 0.191 2,092 yr
10 um 0.019 20,923 yr
100 um 0.002 209,231 yr
1 mm 0.000 2,092,311 yr
Micron grains at 1 AU spiral into the Sun in a few thousand years --
thousands of times shorter than the solar system's age. So the
zodiacal dust cloud cannot be primordial; it is continuously
resupplied by comet trails and asteroid collisions. Grains below the
blow-out size never orbit at all -- they leave as beta meteoroids.
wrote C:\Users\acwic\symplectic-nbody\examples\output\poynting_robertson.svgToomre Q & disk stability
A rotating disk balances self-gravity against pressure (small scales) and rotation via the epicyclic frequency (large scales). Toomre's Q = c_s kappa / (pi G Sigma) captures it in one number: Q > 1 is stable, Q < 1 fragments into clumps and spiral arms. The Milky Way hovers at Q ~ 1.5-2 -- marginally stable, because star formation heats a cooling disk back up, so disks self-regulate to the stability line.
Toomre Q: rotation + pressure vs self-gravity in a disk
Q = c_s kappa / (pi G Sigma) [gas]
Q = sigma_R kappa / (3.36 G Sigma) [stars]
solar neighbourhood (R = 8 kpc):
gas Q = 1.77 (stable)
stellar Q = 1.61 (stable)
Toomre wavelength = 1.46 kpc (the scale of the structures)
R (kpc) kappa (/Gyr) gas Q state
--------------------------------------------
2 159.1 2.61 stable
4 79.6 1.82 stable
6 53.0 1.69 stable
8 39.8 1.77 stable
10 31.8 1.98 stable
14 22.7 2.75 stable
18 17.7 4.17 stable
The Milky Way sits at Q ~ 1.5-2 -- marginally stable. That is no
accident: a disk that cools below Q = 1 fragments into clumps and
spiral arms, and the resulting star formation heats it back up, so
disks self-regulate to hover near the stability line.
wrote C:\Users\acwic\symplectic-nbody\examples\output\toomre.svgAccretion disks: why black holes glow
Gas with angular momentum settles into a disk and spirals in only as viscosity carries momentum outward, dissipating gravitational energy as heat radiated as a blackbody. The Shakura-Sunyaev profile T ~ r^(-3/4) makes the inner edge hottest: a 10-solar-mass hole peaks in soft X-rays (~keV), a billion-solar-mass one in the UV (the quasar 'big blue bump'). Both convert ~6% of rest mass to light -- ~8x fusion.
Shakura-Sunyaev disk: T(r) ~ r^(-3/4), L = eta Mdot c^2
radiative efficiency eta = 0.057 (vs ~0.007 for H fusion)
object r_in (km) peak T (K) peak kT L_Edd (Lsun)
-----------------------------------------------------------------------
stellar-mass BH 88.6 8.64e+06 0.74 keV 3.28e+05
supermassive BH 886237966.5 1.54e+05 13 eV 3.28e+12
The stellar-mass disk peaks around a keV -- soft X-rays, which is how
black-hole binaries are found. The billion-times-heavier supermassive
disk is a thousand times cooler at its edge (T_* ~ M^(-1/4)), peaking
in the ultraviolet: the 'big blue bump' of quasar spectra. Both convert
~6% of infalling rest mass to light -- ~8x more than fusion manages.
wrote C:\Users\acwic\symplectic-nbody\examples\output\accretion_disk.svgFermi acceleration & cosmic rays
A charged particle repeatedly crossing a shock front gains energy at first order in the shock speed each time and has a fixed escape chance, producing a scale-free power-law spectrum N(E) ~ E^(-p). The index depends only on the compression ratio, p = (r+2)/(r-1), and every strong shock converges to r = 4, p = 2 -- the near-universal E^(-2) spectrum injected by supernova remnants across the Galaxy.
Fermi acceleration: shocks stamp a power law N(E) ~ E^(-p)
strong-shock (M -> inf) index: p = 2.00 -> the universal E^(-2) spectrum
Mach compression r index p
--------------------------------
1.5 1.714 5.200
2 2.286 3.333
3 3.000 2.500
5 3.571 2.167
10 3.883 2.040
50 3.995 2.002
1000 4.000 2.000
per-cycle energy gain at beta = 0.03:
first-order (shock): 0.0400 (~4/3 beta)
second-order (clouds): 0.00120 (~4/3 beta^2)
first-order wins by 1/beta ~ 33x -- why shocks dominate.
The magic of diffusive shock acceleration: the index depends only on
the compression ratio, p = (r+2)/(r-1), not on the messy microphysics.
Every strong shock converges to r=4, p=2 -- so supernova remnants across
the Galaxy all inject nearly the same E^(-2) cosmic-ray spectrum.
wrote C:\Users\acwic\symplectic-nbody\examples\output\fermi_acceleration.svgStellar opacity
Opacity kappa sets the photon mean free path 1/(kappa rho) and so how slowly a star leaks its light. Electron scattering is a flat floor in hot ionized gas; Kramers bound-free/free-free absorption rises with density and falls as T^(-7/2), making cool outer layers far more opaque than the core. That steep temperature dependence is what flips stellar envelopes from radiative to convective energy transport.
Stellar opacity: how slowly light escapes matter (kappa, m^2/kg)
electron scattering floor: 0.0338 m^2/kg (T- and rho-independent)
region rho T (K) kappa_es Kramers total
-----------------------------------------------------------------
solar centre 1.5e+05 1.5e+07 0.034 0.074 0.108
radiative zone 2.0e+04 5.0e+06 0.034 0.461 0.494
near surface 1.0e-03 1.0e+05 0.034 0.020 0.054
photosphere 1.0e-04 6.0e+03 0.034 38.468 38.502
Kramers opacity ~ rho T^(-7/2): the cool outer layers are far more
opaque than the blazing core, which is why energy switches from
radiative diffusion to convection in stellar envelopes. In the hot,
dilute deep interior everything drops to the electron-scattering floor
-- the same opacity the Eddington luminosity is built on.
photon mean free path at the solar centre: 61.9 microns
-- a photon random-walks out over ~100,000 years.
wrote C:\Users\acwic\symplectic-nbody\examples\output\opacity.svgBrunt-Vaisala frequency & convection
Displace a fluid parcel upward: if it ends up denser than its new surroundings, gravity pulls it back and it oscillates at the buoyancy frequency N (internal gravity waves, ~5-10 min in the troposphere). If it ends up lighter, buoyancy runs away and the layer convects. N^2 > 0 means stable, N^2 < 0 unstable -- the sign change is exactly the Schwarzschild convection criterion, set by the adiabatic lapse rate.
Brunt-Vaisala buoyancy frequency: N^2 = (g/T)(dT/dz + g/c_p)
dry adiabatic lapse rate g/c_p = 9.76 K/km (the stability threshold)
layer dT/dz (K/km) N (1/s) period state
-----------------------------------------------------------------------
strong inversion 10.0 0.0266 3.9 min stable
stratosphere 2.0 0.0229 4.6 min stable
isothermal 0.0 0.0187 5.6 min stable
typical troposphere -6.5 0.0108 9.7 min stable
dry adiabatic -9.8 0.0000 -- CONVECTIVE
superadiabatic -15.0 0.0000 -- CONVECTIVE
A parcel displaced in a stable layer overshoots and oscillates at N --
these buoyancy (internal gravity) waves ripple through the stratosphere
and the Sun's radiative core. Once the environment cools faster than the
adiabatic lapse rate, N^2 flips negative: buoyancy runs away and the
layer convects. That sign change IS the Schwarzschild convection criterion.
wrote C:\Users\acwic\symplectic-nbody\examples\output\brunt_vaisala.svgRam-pressure stripping
A galaxy plunging through a cluster's hot gas feels a wind of ram pressure rho v^2. The Gunn-Gott criterion strips its interstellar gas wherever that wind beats the disk's gravitational hold 2 pi G Sigma_star Sigma_gas, so the galaxy keeps only the gas inside a stripping radius. In a rich cluster a Milky-Way-like spiral is stripped to a few kpc in one pass -- quenching it into a gas-poor S0.
Ram-pressure stripping (Gunn-Gott): rho_icm v^2 > 2 pi G Sigma_s Sigma_g
Milky-Way-like disk (scale length 3 kpc); gas kept inside R_strip:
environment n (/cc) v (km/s) R_strip (kpc)
---------------------------------------------------------
field / isolated 1e-05 300 16.62
poor group 1e-04 500 11.63
cluster outskirts 5e-04 1000 7.14
rich cluster 1e-03 1500 4.88
dense core, fast 3e-03 2000 2.37
A disk keeps only the gas inside R_strip, where its self-gravity still
beats the cluster wind. In a rich cluster a Milky-Way-like galaxy is
stripped down to a few kpc on a single pass -- its star formation
quenches from the outside in, helping turn infalling spirals into the
gas-poor S0s and ellipticals that crowd cluster cores.
wrote C:\Users\acwic\symplectic-nbody\examples\output\ram_pressure.svgFree-fall: the universal clock of gravity
Remove a body's pressure support and it collapses in the free-fall time t_ff = sqrt(3 pi / 32 G rho) -- which depends only on mean density, not size or mass. So a galaxy and a raindrop of equal density collapse in the same time. The Sun would free-fall in ~30 minutes, a molecular-cloud core in a few hundred kyr, a neutron star in under a millisecond: one 1/sqrt(G rho) line spanning 37 decades of density.
Free-fall time t_ff = sqrt(3 pi / 32 G rho): the clock of gravity
system rho (kg/m^3) t_ff
---------------------------------------------------------
giant molecular cloud 3.85e-19 3.39 Myr
dense cloud core 3.85e-17 339.4 kyr
protostellar core 3.85e-13 3.4 kyr
the Sun (mean) 1.41e+03 29.5 min
the Earth 5.51e+03 14.9 min
white dwarf 1.00e+09 2.1 s
neutron star 5.00e+17 0.09 ms
Notice: t_ff depends only on density, not size or mass. A galaxy and a
raindrop of the same mean density collapse in the same time. That is why
low Earth orbit is always ~90 minutes, why a molecular cloud core forms
stars in a few hundred kyr, and why a neutron star's dynamical time is
well under a millisecond -- denser means faster, universally.
wrote C:\Users\acwic\symplectic-nbody\examples\output\free_fall.svgShock jumps: Rankine-Hugoniot
Move faster than the sound speed c_s = sqrt(gamma P/rho) and the gas cannot get out of the way -- a shock forms. Conservation of mass, momentum and energy fix the jumps from the upstream Mach number: density saturates at 4 (gamma=5/3), but pressure and temperature climb as M^2 without limit, which is why strong shocks heat gas to millions of kelvin while barely compressing it. The downstream flow is always subsonic.
Rankine-Hugoniot shock jumps (gamma = 5/3)
sound speed, ionized gas at 1e4 K: 15.1 km/s
sound speed, air at 288 K: 339 m/s
Mach rho2/rho1 P2/P1 T2/T1 M2 (down)
-------------------------------------------------------
1 1.000 1.0 1.0 1.000
1.5 1.714 2.6 1.5 0.716
2 2.286 4.8 2.1 0.607
3 3.000 11.0 3.7 0.522
5 3.571 31.0 8.7 0.475
10 3.883 124.8 32.1 0.454
30 3.987 1124.8 282.1 0.448
100 3.999 12499.8 3125.9 0.447
Density compression saturates at (gamma+1)/(gamma-1) = 4 -- a strong shock can pack gas only
fourfold. But pressure and temperature jumps grow as M^2 without limit,
which is why strong shocks heat gas to millions of kelvin (supernova
remnants, re-entry plasma) while barely compressing it. The downstream
flow is always subsonic (M2 < 1) -- the shock is a one-way valve.
wrote C:\Users\acwic\symplectic-nbody\examples\output\shock_jump.svgThe Stromgren sphere
A hot star's ultraviolet photons ionize a bubble of hydrogen around it. In equilibrium every ionizing photon replaces one recombination, fixing the Stromgren radius R = (3Q / 4 pi n^2 alpha_B)^(1/3). Because R ~ Q^(1/3) and R ~ n^(-2/3), an O star lights up a ~25 pc nebula in diffuse gas but only a fraction of a parsec in a dense clump -- the pink emission nebulae (Orion, the Rosette) that flag recent massive-star formation.
Stromgren sphere: R_s = (3 Q / 4 pi n^2 alpha_B)^(1/3)
balance: every ionizing photon replaces one recombination
star (Q, /s) n (/cc) R_s (pc) M_ion (Msun)
----------------------------------------------------
O5 (5e49) 10 25.00 16172
O5 (5e49) 100 5.39 1617
O5 (5e49) 1000 1.16 162
O9 (5e48) 10 11.60 1617
O9 (5e48) 100 2.50 162
O9 (5e48) 1000 0.54 16
B0 (1e48) 10 6.79 323
B0 (1e48) 100 1.46 32
B0 (1e48) 1000 0.31 3
R shrinks as n^(-2/3): the same O star lights up a ~25 pc bubble in
diffuse gas but only a fraction of a parsec in a dense clump (a compact
HII region). These are the pink emission nebulae -- Orion, the Rosette --
that flag where massive stars formed in the last few million years.
Switch the star off and the bubble recombines in ~1219 yr.
wrote C:\Users\acwic\symplectic-nbody\examples\output\stromgren.svgTwo-body relaxation & evaporation
Every stellar flyby deflects a star a little; the accumulated random kicks change its velocity by order itself in the relaxation time t_relax ~ (N / 8 ln N) t_cross. Because that grows almost linearly with N, a globular cluster relaxes in ~1 Gyr and slowly evaporates, while a galaxy's relaxation time is millions of Hubble times -- effectively collisionless, which is why it keeps its spiral arms and streams.
Two-body relaxation: t_relax ~ (N / 8 ln N) t_cross
Hubble time = 13.8 Gyr (the collisionless threshold)
system N t_cross t_relax state
---------------------------------------------------------------------
open cluster 1e+03 1956 kyr 35 Myr collisional
globular cluster 1e+05 978 kyr 1062 Myr collisional
nuclear star cluster 1e+07 49 kyr 4 Gyr collisional
dwarf galaxy 1e+08 33 Myr 22117 Gyr collisionless
Milky Way 1e+11 73 Myr 3e+06 t_H collisionless
t_relax grows almost linearly with N, so small systems relax and
mass-segregate fast while big ones never do. A globular cluster relaxes
in ~1 Gyr and slowly evaporates; the Milky Way's relaxation time is
millions of Hubble times, so it is collisionless -- which is why galaxies
keep their spiral arms, streams and cold disks intact for a Hubble time.
wrote C:\Users\acwic\symplectic-nbody\examples\output\relaxation_time.svgThe Parker wind
Parker showed a hot corona cannot stay static: an isothermal atmosphere keeps a finite pressure at infinity, far above interstellar space, so it must expand. The correct steady solution passes smoothly through Mach 1 at the sonic critical radius r_c = GM/2c_s^2 (a few solar radii), staying subsonic inside and supersonic out, and reaches a few hundred km/s by 1 AU -- the solar wind Mariner 2 confirmed.
Parker wind: the transonic solution through the sonic critical point
T (MK) c_s (km/s) r_c (R_sun) v(1 AU) km/s Mach(1 AU)
------------------------------------------------------------
1.0 117.3 6.94 430 3.67
1.5 143.7 4.62 559 3.89
2.0 165.9 3.47 671 4.05
3.0 203.2 2.31 864 4.25
A hotter corona has a larger sound speed but a SMALLER critical radius
(r_c ~ 1/c_s^2), so the wind goes supersonic sooner and reaches a higher
terminal speed. Parker's key point: the only solution that stays finite
at both ends passes smoothly through Mach 1 -- a static corona is
impossible, and the supersonic solar wind confirmed by Mariner 2 follows.
wrote C:\Users\acwic\symplectic-nbody\examples\output\parker_wind.svgThe greenhouse effect
An atmosphere transparent to sunlight but opaque in the infrared lets light in and traps the outgoing heat, so the surface runs hotter than the equilibrium temperature: T_surf = T_eq (1 + 3 tau/4)^(1/4). Earth's modest tau ~ 0.8 lifts 255 K to a life-friendly 288 K; Venus, wrapped in dense CO2 (tau ~ 150), runs away to a lead-melting 737 K; airless Mars sits at its equilibrium temperature.
Greenhouse effect: T_surf = T_eq (1 + 3 tau / 4)^(1/4)
planet T_eq (K) T_surf (K) warming tau needed
----------------------------------------------------
Venus 226.8 737.0 510.2 147.3
Earth 254.7 288.0 33.3 0.8
Mars 209.9 210.0 0.1 0.0
Titan 84.6 94.0 9.4 0.7
Earth's modest tau ~ 0.8 lifts its 255 K skin temperature to a
life-friendly 288 K -- a 33 K blanket. Venus, wrapped in a dense CO2
atmosphere of tau ~ 150, is heated from a 227 K equilibrium to a
lead-melting 737 K: the runaway greenhouse. Nearly airless Mars and
hazy-but-cold Titan sit close to their equilibrium temperatures.
wrote C:\Users\acwic\symplectic-nbody\examples\output\greenhouse.svgRossby number & geostrophic balance
On a rotating planet the Coriolis force deflects moving air at rate f = 2 Omega sin(lat). The Rossby number Ro = U/(fL) decides whether it matters: Ro << 1 (big, slow flows) means geostrophic balance -- wind blows along the isobars, so cyclones and ocean gyres are rotating vortices. Ro >> 1 (tornadoes, bathtub drains) ignores rotation entirely, which is why the draining-sink Coriolis story is a myth.
Rossby number Ro = U / (f L): does planetary rotation matter?
Coriolis parameter at 45 deg: f = 1.03e-04 s^-1
inertial period there: 16.9 hr (half a pendulum day)
flow U (m/s) L Ro regime
---------------------------------------------------------------------
bathtub drain 0.2 0.1 m 1.94e+04 ageostrophic
tornado 100.0 100.0 m 9.7e+03 ageostrophic
sea breeze 5.0 20 km 2.42 ageostrophic
hurricane 50.0 500 km 0.97 ageostrophic
cyclone (synoptic) 10.0 1000 km 0.097 geostrophic
ocean gyre 0.1 2000 km 0.000485 geostrophic
A 1 mb / 100 km pressure gradient drives a ~8 m/s geostrophic wind
-- blowing ALONG the isobars, not across them, because Coriolis balances
the pressure force. Small, fast flows (Ro >> 1) ignore rotation; large,
slow ones (Ro << 1) are ruled by it, which is why weather systems and
ocean gyres are big rotating vortices rather than simple radial flows.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rossby.svgRayleigh-Benard convection
Heat a fluid from below and buoyancy fights viscosity and diffusion. The Rayleigh number Ra = g alpha dT d^3 / (nu kappa) measures the contest: below Ra_c ~ 1708 the layer just conducts, above it convection switches on sharply in rolls and cells. The heat enhancement Nu ~ (Ra/Ra_c)^(1/3) climbs steeply -- and at the Ra ~ 10^30 of the solar convection zone the flow is violently turbulent, driving granulation and mantle plate motion.
Rayleigh-Benard: Ra = g alpha dT d^3 / (nu kappa); onset at Ra_c ~ 1708
system Ra state Nu
----------------------------------------------------------
lab cell (near onset) 3.6e+01 conducting 1
mug of coffee 4.7e+07 convecting 30
pot on a stove 3.6e+08 convecting 60
Earth's mantle 2.2e+08 convecting 50
solar convection zone 7.8e+30 convecting 2e+09
Below Ra_c the layer just conducts (Nu = 1); above it convection
switches on sharply and carries far more heat (Nu ~ (Ra/Ra_c)^(1/3)).
Astrophysical layers -- the mantle, the solar convection zone -- run
at Ra of 10^20 or more, so they are violently, turbulently convective:
the granulation on the Sun and the plates under our feet both follow.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rayleigh_benard.svgTerminal velocity & drag
A falling body speeds up until drag balances gravity. Which drag law applies is set by the Reynolds number: viscous Stokes drag (v ~ r^2) for tiny slow particles, quadratic drag (v ~ sqrt(r)) for big fast ones. So a fog droplet 200x smaller than a raindrop falls 40000x slower and effectively floats, a raindrop settles at ~9 m/s, and a belly-down skydiver tops out near 50 m/s.
Terminal velocity: drag balances gravity (regime set by Reynolds number)
object v_term Re regime
--------------------------------------------------------
fog droplet 10um 12.03 mm/s 0.016 Stokes
drizzle 100um 2.1 m/s 29 quadratic
raindrop 2mm 9.5 m/s 2.6e+03 quadratic
hailstone 1cm 20.2 m/s 2.7e+04 quadratic
steel ball 1cm 59.5 m/s 8.1e+04 quadratic
skydiver (belly) 49 m/s (~176 km/h)
Stokes drag (viscous) gives v ~ r^2, so a fog droplet 200x smaller than
a raindrop falls ~40000x slower -- effectively floating. Big drops cross
into quadratic drag where v ~ sqrt(r), so a hailstone and a steel ball of
the same size differ only by sqrt(density). The regime boundary is Re ~ 1.
wrote C:\Users\acwic\symplectic-nbody\examples\output\terminal_velocity.svgSupernova remnant phases
A supernova dumps ~10^51 erg into the ISM, and the shell ages through four phases: ballistic free expansion for a few centuries, then the long adiabatic Sedov-Taylor phase (R ~ t^2/5) for tens of thousands of years, then a radiative snowplow coasting on momentum, finally merging into the ISM near 100 pc after ~10^6 yr -- seeding the galaxy with the elements it forged along the way.
Supernova remnant: free expansion -> Sedov -> snowplow -> merge
E = 1e51 erg, M_ej = 5 Msun, n = 1 /cc
sweep-up radius (free expansion ends): 3.3 pc at 318 yr
merge radius (~10 km/s): 167 pc
age (yr) R (pc) v (km/s) phase
--------------------------------------------------
100 1.0 10000 free expansion
300 3.1 10000 free expansion
1000 5.0 1947 Sedov-Taylor
5000 9.5 741 Sedov-Taylor
20000 16.5 323 Sedov-Taylor
50000 23.8 186 snowplow
200000 41.4 81 snowplow
1000000 78.9 31 merged
The blast coasts ballistically for a few centuries, then sweeps up
enough gas to enter the long adiabatic Sedov phase (R ~ t^2/5) for tens
of thousands of years. When the shell cools it radiates and coasts on
momentum (snowplow), finally merging into the ISM near 100 pc after a
million years -- seeding the galaxy with the elements it forged.
wrote C:\Users\acwic\symplectic-nbody\examples\output\snr_phases.svgThe magnetic mirror & loss cone
A charged particle spiraling along a field line conserves its magnetic moment mu = m v_perp^2 / 2B. Drifting into stronger field, v_perp must grow and v_parallel shrink until the particle reflects -- a magnetic mirror. Trapping depends only on pitch angle: sin^2(alpha) > 1/R_m holds the particle, else it falls into the loss cone and escapes. This traps the Van Allen belts and lights the aurora.
Magnetic mirror: trapped if sin^2(pitch) > B_min/B_max = 1/R_m
mirror ratio R_m loss cone (deg) 20 deg 60 deg
------------------------------------------------------
2 45.0 lost trap
4 30.0 lost trap
10 18.4 trap trap
50 8.1 trap trap
1000 1.8 trap trap
A bigger mirror ratio means a narrower loss cone, so more particles are
held. Earth's dipole (R_m ~ tens between equator and pole) traps the Van
Allen belts: particles bounce pole to pole, reflected where the field
tightens, unless their pitch angle drops them into the loss cone and down
into the atmosphere -- which is what lights up the aurora.
wrote C:\Users\acwic\symplectic-nbody\examples\output\magnetic_mirror.svgDebye shielding & the plasma frequency
Ionized gas screens any test charge within the Debye length lambda_D = sqrt(eps0 kT / n e^2), and behaves as a collective plasma only when many particles sit inside a Debye sphere. Disturb the electrons and they ring at the plasma frequency omega_p = sqrt(n e^2 / eps0 m_e); waves below it are reflected -- which is why the ionosphere's ~9 MHz cutoff bounces AM radio around the Earth but lets FM escape to space.
Debye model: C_V rises as T^3, plateaus at Dulong-Petit 3R = 24.94 J/mol/K
material Theta_D (K) C_V @300K % of 3R
------------------------------------------------
lead 105 24.79 99.4%
copper 343 23.39 93.8%
aluminium 428 22.58 90.5%
diamond 2230 4.13 16.6%
Every solid follows one universal curve in T/Theta_D. A stiff, light
lattice has a high Debye temperature, so at room temperature it is still
'cold' -- diamond stores only ~1/6 of its classical heat capacity, while
soft heavy lead has long since reached the 3R plateau. The T^3 falloff at
low temperature is the fingerprint of phonon quantization.
wrote C:\Users\acwic\symplectic-nbody\examples\output\debye_heat.svgSpectral line broadening
Atomic lines have width from three causes. Thermal Doppler motion gives a Gaussian of width ~ sqrt(T/m) -- the standard plasma thermometer. The finite excited-state lifetime gives an irreducible natural (Lorentzian) width A/4pi. Collisions add a pressure (Lorentzian) width that grows with density, so dense dwarf photospheres show broad wings absent in thin gas. The observed profile is their Voigt convolution.
Line broadening of H-alpha (656.3 nm): Doppler / natural / pressure
natural width (fixed): 5.2 MHz
environment T (K) Doppler pressure dominant
-------------------------------------------------------------------
HII region 10000 19.57G 0.000G Doppler
solar photosphere 6000 15.16G 0.159G Doppler
red giant 4000 12.38G 0.002G Doppler
white-dwarf atmos 10000 19.57G 1591.549G pressure
cool ISM cloud 100 1.96G 0.000G Doppler
Doppler width ~ sqrt(T/m) reads the temperature (and, from the ratio of
species widths, tells light atoms from heavy). Pressure width grows with
density, so a dense white-dwarf photosphere has hugely broadened,
Lorentzian-winged lines while a thin HII region is purely Doppler. The
observed shape is the Voigt convolution of the Gaussian and Lorentzian.
wrote C:\Users\acwic\symplectic-nbody\examples\output\line_broadening.svgThe curve of growth
How an absorption line's equivalent width grows with column density has three regimes: linear (W ~ N) for weak lines, a flat saturated plateau once the core goes black (W barely moves over decades of N), and a square-root damping tail (W ~ sqrt(N)) when the Lorentzian wings go thick. Matching a measured equivalent width to this curve is how stellar abundances are read from spectra.
Curve of growth: equivalent width vs central optical depth
tau0 W / dnu_D regime
----------------------------------------
0.01 0.01 linear
0.1 0.1 linear
1 1 saturated
10 3.097 saturated
100 4.297 saturated
1000 5.257 saturated
100000 17.72 damped
1e+07 177.2 damped
1e+09 1772 damped
Weak lines grow linearly (W ~ N): every atom adds absorption. Once the
core saturates the line is already black, so W barely moves over decades
of column density -- the flat part where abundances are hardest to pin
down. At enormous columns the Lorentzian damping wings go optically thick
and W ~ sqrt(N) again. Matching a measured W to this curve is how stellar
abundances are read off absorption spectra.
wrote C:\Users\acwic\symplectic-nbody\examples\output\curve_of_growth.svgSackur-Tetrode entropy
Quantum state-counting fixes the absolute entropy of an ideal gas: S = N k_B [ln((V/N)(4 pi m U / 3 N h^2)^(3/2)) + 5/2]. Planck's constant enters explicitly (a phase-space cell is h^3) and the 1/N! for indistinguishable atoms resolves the Gibbs paradox. Evaluated for the noble gases at STP it reproduces the measured standard molar entropies to under a fifth of a percent -- entropy really is log of microstates.
Sackur-Tetrode: absolute entropy from counting quantum microstates
standard molar entropy at STP (298.15 K, 1 atm):
gas mass (amu) S predicted S measured error
------------------------------------------------------
He 4.003 126.0 126.2 -0.12%
Ne 20.180 146.2 146.3 -0.06%
Ar 39.948 154.7 154.8 -0.04%
Kr 83.798 164.0 164.1 -0.08%
Xe 131.290 169.6 169.7 -0.07%
Predicted from nothing but atomic mass, T and P -- and matching the
calorimetric values to a fraction of a percent. The quantum of action h
appears explicitly (a phase-space cell is h^3), and the 1/N! for
indistinguishable atoms is what makes entropy extensive and resolves the
Gibbs paradox. Entropy really is the logarithm of countable microstates.
wrote C:\Users\acwic\symplectic-nbody\examples\output\sackur_tetrode.svgMaxwell-Boltzmann speeds
Molecular speeds in a gas follow f(v) ~ v^2 exp(-mv^2/2kT), whose three characteristic speeds keep a fixed ratio v_p :
Maxwell-Boltzmann speeds at T = 300 K (mean KE = 0.039 eV)
v_p : <v> : v_rms = 1 : 1.128 : 1.225 (universal)
gas mass (amu) v_p <v> v_rms >3 v_p
---------------------------------------------------------
H2 2 1579 1782 1934 4.4e-04
He 4 1117 1260 1368 4.4e-04
N2 28 422 476 517 4.4e-04
O2 32 395 446 484 4.4e-04
CO2 44 337 380 412 4.4e-04
Speeds scale as 1/sqrt(m), so hydrogen zips along four times faster than
nitrogen at the same temperature -- which is why light gases escape
atmospheres and why sound (set by ~v_rms) travels faster in helium. The
fraction above 3 v_p is only ~0.04%: the exp(-v^2) tail is thin, but it
is exactly that tail that lets atoms escape gravity and nuclei fuse.
wrote C:\Users\acwic\symplectic-nbody\examples\output\maxwell_boltzmann.svgThe Gamow peak
Nuclei must beat an MeV Coulomb barrier to fuse, yet the Sun's core is only ~1.3 keV. Two factors save it: the Maxwell-Boltzmann tail exp(-E/kT) falls with energy while quantum tunnelling exp(-sqrt(E_G/E)) rises, and their product is sharply peaked at the Gamow energy E0 = (E_G (kT)^2/4)^(1/3). Solar p-p fusion happens in a narrow window at ~6 keV; heavier nuclei need far hotter cores, the thermostat of stellar burning.
Gamow peak: Boltzmann tail x quantum tunnelling = fusion window
solar core kT = 1.29 keV; barrier ~ MeV -- yet stars burn
reaction Z1 Z2 T (K) E_G (keV) peak E0
----------------------------------------------------
p + p 1 1.5e+07 493 5.9k
p + N14 7 1.5e+07 45105 26.6k
He + He 4 1.0e+08 31560 83.7k
C + C 36 5.0e+08 7669118 1526.8k
Fusion lives in a narrow window far out on the thermal tail: the Sun's
p-p peak sits at ~6 keV, several times the mean 1.3 keV, where enough
fast protons meet a high-enough tunnelling chance. Because the peak
climbs steeply with nuclear charge, carbon burning needs ~500 million K
while hydrogen ignites at 15 million -- the thermostat of stellar life.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gamow.svgParallax & proper motion
As Earth orbits the Sun a nearby star shifts by the parallax angle p, and the parsec is defined so d (pc) = 1/p (arcsec) -- the first rung of the distance ladder Gaia climbed for a billion stars. Proper motion adds the sideways drift: v_t = 4.74 mu d, combined with the Doppler radial velocity into the space velocity. Barnard's Star, the fastest, moves at 142 km/s through space.
Parallax: d (pc) = 1 / p (arcsec); v_t (km/s) = 4.74 mu d
star p (") d (pc) d (ly) mu ("/yr) v_space
----------------------------------------------------------------
Proxima Cen 0.7687 1.30 4.2 3.85 32.6k
Barnard's Star 0.5469 1.83 6.0 10.36 142.0k
Sirius 0.3792 2.64 8.6 1.34 17.6k
Vega 0.1305 7.66 25.0 0.35 18.8k
Betelgeuse 0.0055 181.82 593.0 0.03 33.9k
Parallax is pure geometry -- the star's apparent shift over Earth's orbit
-- and the first rung of the distance ladder Gaia has now climbed for over
a billion stars. Proper motion adds the sideways drift: Barnard's Star,
the fastest, crosses a Moon's width of sky every ~180 years and moves at
142 km/s through space once its radial and tangential motions combine.
wrote C:\Users\acwic\symplectic-nbody\examples\output\parallax.svgStandard candles & the distance ladder
Know an object's true luminosity and its apparent brightness gives its distance: m - M = 5 log10(d/10 pc). Cepheids supply M through Leavitt's period-luminosity law, Type Ia supernovae (M ~ -19.3) extend it to hundreds of Mpc, and chaining parallax -> Cepheids -> supernovae is the cosmic distance ladder. Five magnitudes is exactly 100x in flux.
Standard candles: m - M = 5 log10(d/10 pc); brightness gives distance
object distance modulus
----------------------------------------------
10 pc (M = m) 10 pc 0.00
Hyades cluster 47 pc 3.36
Galactic centre 8200 pc 14.57
LMC 50 kpc 18.49
Andromeda (M31) 778 kpc 24.45
Virgo cluster 16.5 Mpc 31.09
SN Ia horizon 1000.0 Mpc 40.00
Cepheid period-luminosity (Leavitt's law): longer period = brighter
period (d) M_V
3 -2.77
10 -4.24
30 -5.58
100 -7.05
Five magnitudes is exactly 100x in flux. Knowing M -- from a Cepheid's
pulsation period or a Type Ia's standardizable peak (M ~ -19.3) -- turns
the apparent brightness into a distance. Chaining parallax to Cepheids to
supernovae is the ladder that measures the expanding universe.
wrote C:\Users\acwic\symplectic-nbody\examples\output\standard_candle.svgThe Tully-Fisher relation
Spiral galaxies obey a tight L ~ v_flat^4 scaling: because v^2 = GM/R and spirals hold roughly constant surface brightness, mass, spin and light rise together. A Milky-Way-like spiral (v ~ 220 km/s) shines ~3x10^10 L_sun. Since the rotation width is easy to measure from the 21-cm line, Tully-Fisher is a redshift-independent distance indicator reaching far beyond resolvable Cepheids -- the spiral cousin of Faber-Jackson.
Tully-Fisher: L ~ v_flat^4 -- spirals that spin faster shine brighter
galaxy type v_flat L (Lsun) M_abs M_baryon
------------------------------------------------------------
dwarf spiral 80 5.12e+08 -14.38 2.05e+09
small spiral 120 2.59e+09 -16.05 1.04e+10
Milky Way-like 220 2.93e+10 -18.55 1.17e+11
massive spiral 300 1.01e+11 -19.83 4.05e+11
giant spiral 400 3.20e+11 -21.02 1.28e+12
A four-fold jump in luminosity for every doubling of rotation speed --
because v^2 = GM/R and spirals hold roughly constant surface brightness,
so mass, spin and light rise together. Since the rotation width is easy
to measure (the 21-cm line), Tully-Fisher gives redshift-independent
distances far beyond where individual Cepheids can be resolved.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tully_fisher.svgThe Tolman dimming test
Surface brightness is distance-independent in a static universe -- flux and angular area fall together. Expansion breaks that with four factors of (1+z), so SB ~ (1+z)^-4: a z=1 galaxy is dimmed 16x per square arcsecond, a z=3 galaxy 256x. A static tired-light universe would dim only as (1+z); observations back the (1+z)^4 law -- direct evidence the redshift is real expansion, not photons losing energy en route.
Tolman test: surface brightness ~ (1+z)^-4 if the universe expands
z expanding mag tired-light ratio E/T
-----------------------------------------------------
0.2 1/2 0.79 1/1.2 0.579
0.5 1/5 1.76 1/1.5 0.296
1.0 1/16 3.01 1/2.0 0.125
2.0 1/81 4.77 1/3.0 0.037
3.0 1/256 6.02 1/4.0 0.016
5.0 1/1296 7.78 1/6.0 0.005
Surface brightness is distance-independent in a static Euclidean universe
-- flux and angular area fall together. Expansion breaks that with four
factors of (1+z): redshift, time dilation, and the D_A/D_L geometry. So a
z=1 galaxy is dimmed 16x per square arcsecond, a z=3 galaxy 256x. A
tired-light universe would dim only as (1+z); observations back the (1+z)^4,
direct evidence the redshift is genuine expansion, not photons tiring out.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tolman.svgOlbers' paradox
In an infinite, eternal, static universe every line of sight would end on a star and the whole sky would blaze. It does not -- the night is dark. Stars would tile the sky only after ~10^16 light-years (the mean free path 1/n sigma), but the cosmic horizon (c x age) is a million times closer, so only ~10^-6 of the sky is covered. The finite age of the universe, not infinite space, is what makes night dark.
Olbers' paradox: in an infinite static universe the sky would blaze
star density ~ 0.1 /pc^3 -> mean free path to a star = 2.0e+16 ly
cosmic horizon (c x age) = 1.4e+10 ly
ratio horizon / mfp = 6.8e-07
distance d / mfp sky covered
--------------------------------------------
cosmic horizon 6.76e-07 6.757e-07
100x horizon 6.76e-05 6.757e-05
0.1 mfp 1.00e-01 0.09516
1 mfp 1.00e+00 0.6321
5 mfp 5.00e+00 0.9933
20 mfp 2.00e+01 1
Within the observable universe only ~7e-07
of the sky is covered by stellar disks -- which is why night is dark. The
paradox is real: every line of sight WOULD end on a star, but only after
~10^16 light-years, a million times farther than light has travelled since
the Big Bang. The finite age of the universe, not infinite space, saves us.
wrote C:\Users\acwic\symplectic-nbody\examples\output\olbers.svgBi-elliptic transfer
The Hohmann two-burn transfer is cheapest for modest orbit changes, but for large radius ratios a three-burn bi-elliptic transfer -- flinging the craft far beyond the target and dropping back -- costs less total delta-v, because the mid-course burn happens where orbital speeds are tiny. Below R = 11.94 Hohmann always wins; above R = 15.58 bi-elliptic always does. The saving is paid for with a much longer, sometimes years-long, transfer.
Hohmann vs bi-elliptic transfer (r_b = 100 r1 detour)
crossover ratio R = r2/r1 = 11.94
R = r2/r1 Hohmann dv bi-elliptic winner
---------------------------------------------------
5.00 3622m 4482m Hohmann
10.00 3998m 4120m Hohmann
11.94 4030m 4050m Hohmann
13.00 4039m 4020m bi-elliptic
16.00 4046m 3953m bi-elliptic
30.00 3980m 3810m bi-elliptic
60.00 3836m 3735m bi-elliptic
For modest orbit changes the Hohmann two-burn is unbeatable. Past R ~ 12,
flinging the craft far beyond the target and dropping back costs less
total delta-v, because the mid-course burn happens where orbital speeds
are tiny. The catch: the detour can take years, so bi-elliptic is used
only for the most extreme orbit raises. The 11.94 crossover is exact.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bi_elliptic.svgGravity assist
A spacecraft flying past a planet follows a hyperbola: it leaves at the same speed relative to the planet but bent by the turn angle sin(delta/2) = 1/e. In the Sun's frame the planet is moving, so that rotation adds up to 2 v_inf of free heliocentric speed. A slower, deeper pass bends more and steals more; Voyager chained Jupiter-Saturn-Uranus-Neptune this way to reach escape speed for almost no fuel.
Gravity assist: bend v_inf in the planet's frame, gain in the Sun's
Jupiter orbital speed 13.1 km/s; max possible gain = 2 v_inf
v_inf (km/s) periapsis turn (deg) boost (km/s)
---------------------------------------------------
5 2 R_jup 153.1 9.73
5 5 R_jup 138.2 9.34
10 2 R_jup 127.9 17.97
10 5 R_jup 102.5 15.60
15 2 R_jup 105.8 23.92
15 5 R_jup 75.4 18.35
A slower, deeper pass swings the velocity vector through a bigger angle
and adds more of Jupiter's 13 km/s orbital motion -- up to 2 v_inf for a
near-reversal. Voyager 2 chained Jupiter, Saturn, Uranus and Neptune this
way to reach Solar-System escape speed on a fraction of the fuel a direct
burn would need, while the planets lost a laughably tiny bit of energy.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gravity_assist.svgSynodic periods & alignments
The geometry we see -- oppositions, launch windows, new Moons -- repeats on the synodic period, the beat between two orbital rates: 1/S = |1/P1 - 1/P2|. Mars returns to opposition every ~780 days, exactly the ~26-month cadence of Mars missions; the synodic month is 29.5 days, longer than the 27.3-day sidereal month. Near Earth's own orbit S blows up; distant planets approach a one-year synodic period.
Synodic period: 1/S = |1/P_planet - 1/P_earth| -- when alignments repeat
planet sidereal (d) synodic (d) per year
------------------------------------------------
Mercury 88.0 115.9 3.152
Venus 224.7 583.9 0.626
Mars 687.0 779.9 0.468
Jupiter 4332.6 398.9 0.916
Saturn 10759.2 378.1 0.966
Neptune 60190.0 367.5 0.994
synodic month (new Moon to new Moon): 29.53 days
Mars returns to opposition every ~780 days -- exactly the ~26-month
cadence of Mars launch windows. Inner planets lap Earth quickly; distant
planets barely move, so their synodic period approaches one Earth year
(Earth does the lapping). Near Earth's own orbit the synodic period blows
up: two bodies at the same distance never change their alignment.
wrote C:\Users\acwic\symplectic-nbody\examples\output\synodic.svgThe black-hole shadow
A black hole casts a dark disk larger than its horizon: light inside the critical impact parameter b = 3 sqrt(3) GM/c^2 is captured, so lensing magnifies the shadow to 5.2 Schwarzschild radii across (vs 2 r_s for the horizon). M87* and Sgr A* each subtend only ~40-50 microarcseconds -- the angular size of an orange on the Moon -- which is why the Event Horizon Telescope had to link radio dishes across the whole Earth.
Black-hole shadow: apparent size 3 sqrt(3) r_s = 5.2 r_s (EHT)
object mass (Msun) distance shadow (uas)
--------------------------------------------------
M87* 6.50e+09 16.8 Mpc 39.7
Sgr A* 4.15e+06 8.15 kpc 52.2
stellar BH 1.00e+01 3 kpc 3.4e-04
shadow diameter = 5.196 r_s (vs 2 r_s for the horizon)
Light is captured inside the critical impact parameter b = 3 sqrt(3) GM/c^2,
so the dark disk is bigger than the event horizon -- gravitational lensing
magnifies it. M87* and Sgr A* both subtend only ~40-50 microarcseconds, the
angular size of an orange on the Moon, which is why the EHT had to link radio
dishes across the whole Earth to resolve them.
wrote C:\Users\acwic\symplectic-nbody\examples\output\black_hole_shadow.svgThe Hill sphere
A moon is held by its planet only inside the Hill sphere, r_H = a (m/3M)^(1/3), where the planet's pull beats the star's tide. Earth's is ~1.5 million km, four times the Moon's distance; Jupiter's reaches ~53 million km. Real moons survive out to ~1/2 r_H prograde, and the same balance sets the feeding zone of a forming planet and the mutual Hill spacing that keeps planetary orbits stable.
Hill sphere: r_H = a (m/3M)^(1/3) -- how far a planet holds its moons
planet a (AU) mass (Me) r_H (Mkm) stable limit
--------------------------------------------------------
Mercury 0.387 0.055 0.22 0.11M
Venus 0.723 0.815 1.01 0.51M
Earth 1.000 1.000 1.50 0.75M
Mars 1.524 0.107 1.08 0.54M
Jupiter 5.203 317.800 53.13 26.57M
Saturn 9.537 95.200 65.16 32.58M
Neptune 30.070 17.100 115.93 57.96M
A bigger orbit or heavier planet widens the Hill sphere (r_H ~ a m^1/3),
so Jupiter commands a ~53-million-km domain while a hot Jupiter tucked
against its star could barely hold a moon. Real moons survive out to only
~1/2 r_H prograde -- the Moon at 0.384 Mkm sits well inside Earth's 0.75
Mkm limit, and the same math sets the mutual spacing of planetary orbits.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hill_sphere.svgJ2 orbital precession
A planet's equatorial bulge (coefficient J2) makes satellite orbits precess: the node line regresses and the ellipse rotates in-plane. Tune the inclination so the nodal drift matches the Sun's 0.9856 deg/day and you get a sun-synchronous orbit (~98 deg) crossing the equator at fixed local time; at the 63.4-degree critical inclination the apsides freeze -- the Molniya orbit that parks apogee over the far north.
J2 orbital precession (Earth, 700 km circular orbit)
orbit incl (deg) nodal (deg/d) apsidal (deg/d)
------------------------------------------------------------------
equatorial 0.00 -6.921 13.842
ISS-like 51.60 -4.299 3.215
critical (Molniya) 63.43 -3.095 0.000
sun-synchronous 98.19 0.986 -3.110
polar 90.00 -0.000 -3.461
The equatorial bulge (J2) drags orbit planes and rotates ellipses. Tune
the inclination so the nodal drift equals the Sun's 0.9856 deg/day and the
orbit stays fixed relative to the Sun -- a sun-synchronous orbit crossing
the equator at the same local time daily. At 63.4 deg the apsides freeze
(Molniya), parking apogee over the far north for long dwell times.
wrote C:\Users\acwic\symplectic-nbody\examples\output\j2_precession.svgSolar sails & radiation pressure
Sunlight carries momentum: a mirror at 1 AU feels ~9 uPa (2F/c), a feather touch that never runs out. Since both sunlight and gravity fall as 1/r^2, the lightness number beta = radiation force / solar gravity is a fixed property of the sail; beta = 1 (a ~1.5 g/m^2 mirror) cancels the Sun's pull and beta > 1 escapes on sunlight alone. Today's sails sit at beta ~ 0.01 -- gentle but propellant-free.
Solar sail: sunlight pressure 2F/c on a mirror, ~9 uPa at 1 AU
solar flux at 1 AU: 1361 W/m^2, mirror pressure 9.08 uPa
sail area (m^2) mass (kg) accel (mm/s^2) beta
-----------------------------------------------------------------
IKAROS 196 315 0.00565 0.000953
LightSail 2 32 5 0.05812 0.0098
NEA Scout 86 14 0.05578 0.0094
Starshot chip 16 0.001 145.3 24.5
beta = 1 sail 653 1 5.93 1
Both sunlight and gravity fall as 1/r^2, so the lightness number beta =
radiation force / solar gravity is a fixed property of the sail. beta = 1
(area-to-mass ~653 m^2/kg, a ~1.5 g/m^2 mirror) exactly cancels the Sun's
pull; beta > 1 escapes the Solar System on sunlight alone. Today's sails
sit at beta ~ 0.01 -- gentle, but propellant-free and endless.
wrote C:\Users\acwic\symplectic-nbody\examples\output\solar_sail.svgRelativistic beaming
Radiation from a source moving near light speed is swept forward into a cone of half-angle ~1/gamma and Doppler-boosted, so the observed flux scales as D^(3+alpha). An approaching jet is brightened hundreds of times while its receding twin is dimmed by the same powers -- why M87's jet looks one-sided. The same geometry makes blobs appear to move faster than light, an illusion of light-travel time.
Relativistic beaming: D = 1/(gamma(1 - beta cos theta)); flux ~ D^(3+a)
gamma theta Doppler D flux boost jet/cj v_app/c
------------------------------------------------------------
2 5d 3.64 119 1.6e+04 0.5
2 20d 2.69 38.7 4.5e+03 1.6
5 5d 8.36 2.58e+03 1.2e+07 3.6
5 20d 2.52 30.7 1.3e+05 4.2
10 5d 11.37 8.05e+03 5.2e+08 9.9
10 20d 1.54 4.92 2.8e+05 5.2
Aberration sweeps the emission into a cone of half-angle ~1/gamma, and the
Doppler shift boosts the flux by D^(3+alpha). An approaching jet is brightened
hundreds of times while its receding twin is dimmed by the same powers, which
is why M87's jet looks one-sided. The same geometry makes blobs appear to move
faster than light -- superluminal motion, an illusion of light-travel time.
wrote C:\Users\acwic\symplectic-nbody\examples\output\beaming.svgThe relativistic rocket
A ship at constant 1 g follows a hyperbolic worldline: velocity c tanh(a tau/c) saturates just short of c, but proper time uses cosh/sinh, so the crew clock falls ever further behind Earth's. The galactic centre is ~10 crew-years away (27,000 pass on Earth), Andromeda ~15 -- ship time grows only logarithmically with distance. The catch is fuel: a photon drive needs exp(2 phi) times the payload mass.
Relativistic rocket at constant 1 g (acceleration only)
destination distance ship (yr) Earth (yr) v_peak/c
------------------------------------------------------------------
Proxima Centauri 4.37 ly 2.3 5.25 0.9834
Vega 25.00 ly 3.9 26 0.9993
Galactic centre 27 kly 10.6 2.7e+04 1.0000
Andromeda (M31) 2.5 Mly 15.0 2.5e+06 1.0000
edge of observable 4.6e+04 Mly 24.5 4.6e+10 1.0000
Velocity is c tanh(a tau/c) -- it saturates just short of c -- but proper
time uses cosh/sinh, so the crew's clock falls ever further behind Earth's.
A 1-g ship reaches the galactic centre in ~10 crew-years (27,000 pass on
Earth) and could cross to Andromeda in ~15. The impossible part is fuel: a
photon drive needs exp(2 phi) times the payload mass, astronomically much.
wrote C:\Users\acwic\symplectic-nbody\examples\output\relativistic_rocket.svgRelativistic Doppler shift
A moving light source shifts in frequency by the classical Doppler effect times time dilation: receding sources redshift, approaching ones blueshift. The purely relativistic surprise is the transverse shift -- a source moving exactly across the line of sight still reddens by 1/gamma because its clock runs slow, the effect Ives and Stilwell measured in 1938. A measured redshift maps straight back to a speed.
Relativistic Doppler: classical shift x time dilation
beta receding z approaching z transverse z
--------------------------------------------------
0.10 0.1055 -0.0955 0.0050
0.30 0.3628 -0.2662 0.0483
0.50 0.7321 -0.4226 0.1547
0.70 1.3805 -0.5799 0.4003
0.90 3.3589 -0.7706 1.2942
0.99 13.1067 -0.9291 6.0888
Receding sources redshift, approaching ones blueshift -- the familiar
Doppler shift, but amplified by time dilation. The purely relativistic
effect is the transverse shift: a source moving exactly across the line of
sight still reddens by 1/gamma because its clock runs slow. Ives and
Stilwell measured it in 1938 -- direct proof of time dilation. A measured
redshift maps back to a speed: z = 1 means beta = 0.60.
wrote C:\Users\acwic\symplectic-nbody\examples\output\relativistic_doppler.svgde Broglie matter waves
Every particle has a wavelength lambda = h/p, inversely proportional to its momentum. A baseball's is 10^-34 m -- undetectable -- but a 100 keV electron's is ~4 pm, thousands of times finer than light, which is why electron microscopes resolve atoms; a thermal neutron's ~0.1 nm matches crystal spacing for diffraction. The wave nature takes over once lambda rivals the interparticle spacing -- the onset of quantum degeneracy.
de Broglie wavelength: lambda = h / p -- everything is a wave
object lambda
--------------------------------------------
100 keV microscope e- 3.88 pm
1 eV electron 1.23 nm
thermal neutron (300 K) 100 pm
thermal He atom (300 K) 50.2 pm
100 m/s N2 molecule 141 pm
baseball (40 m/s) 1.14e-34 m
Momentum sets the wavelength, so heavy or fast things have vanishingly
short waves -- a baseball's is 10^-34 m, undetectable. But a 100 keV
electron's 4 pm is thousands of times finer than visible light, which is
why electron microscopes resolve atoms, and a thermal neutron's ~0.1 nm
matches crystal spacing, making neutron diffraction a structural probe.
wrote C:\Users\acwic\symplectic-nbody\examples\output\de_broglie.svgThe Bohr model of hydrogen
Quantizing angular momentum (L = n hbar) forces the electron onto discrete orbits: energy E_n = -13.6/n^2 eV, radius n^2 a_0 (a_0 ~ 52.9 pm). Transitions emit fixed-energy photons -- the sharp hydrogen lines: Lyman in the UV, Balmer in the visible (H-alpha at 656 nm, the red of nebulae), Paschen in the infrared. The n=1 orbital speed is alpha*c, v/c ~ 1/137.
Bohr model of hydrogen: E_n = -13.6/n^2 eV, r_n = n^2 a_0
Bohr radius a_0 = 52.92 pm, fine-structure alpha = 1/137.0
n E_n (eV) r_n (pm)
--------------------------
1 -13.606 52.9
2 -3.401 211.7
3 -1.512 476.3
4 -0.850 846.7
5 -0.544 1322.9
spectral series (to lower level n1):
Lyman (UV) to n=1: 121.5, 102.5, 97.2 nm ...
Balmer (visible) to n=2: 656.1, 486.0, 433.9 nm ...
Paschen (IR) to n=3: 1874.6, 1281.5, 1093.5 nm ...
Quantizing angular momentum (L = n hbar) forces the electron onto discrete
orbits, so it can only emit photons of fixed energy -- the sharp lines of
the hydrogen spectrum. H-alpha at 656 nm gives nebulae their red glow;
the Lyman series lands in the UV, Paschen in the infrared. The model nails
the energies exactly, and v/c at n=1 is the fine-structure constant.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bohr.svgThe photoelectric effect
Light ejects electrons from a metal only above a threshold frequency, no matter how bright a redder beam is -- Einstein's proof that light comes in photons of energy hf. One photon gives one electron K_max = hf - phi, so the stopping voltage climbs linearly with frequency at the universal slope h/e; only the intercept (the work function) differs between metals. Millikan measured that line and pinned down Planck's constant.
Photoelectric effect: K_max = h f - phi (Einstein 1905)
slope of V_stop vs f is h/e = 4.136e-15 V/Hz (universal)
metal phi (eV) threshold V_stop @254nm V_stop @400nm
--------------------------------------------------------------
cesium 2.14 579nm 2.74 V 0.96 V
sodium 2.28 544nm 2.60 V 0.82 V
zinc 4.31 288nm 0.57 V none
platinum 6.35 195nm none none
Below the threshold frequency, no electrons come off however bright the
light -- energy comes in photon lumps, not a continuous wave. Above it, the
stopping voltage climbs linearly with frequency at the SAME slope h/e for
every metal; only the intercept (the work function) differs. Millikan
measured exactly this line and pinned down Planck's constant.
wrote C:\Users\acwic\symplectic-nbody\examples\output\photoelectric.svgThe uncertainty principle
Position and momentum cannot both be sharp: dx dp >= hbar/2. Confining a particle to a box forces a momentum spread and thus an irreducible zero-point energy E ~ hbar^2/(m dx^2). An electron squeezed to atomic size (~0.1 nm) carries ~1 eV -- why it never falls into the nucleus -- and minimizing that against the Coulomb pull reproduces hydrogen's 13.6 eV; a nucleon in a femtometre nucleus carries MeV.
Heisenberg: dx dp >= hbar/2 -> irreducible confinement energy
confinement size electron E nucleon E
----------------------------------------------------------
molecule 1 nm 0.0095 eV 5.2e-06 eV
atom 0.1 nm 0.95 eV 0.00052 eV
inner atom 0.01 nm 95 eV 0.052 eV
atomic nucleus 5 fm 3.8e+02 MeV 0.21 MeV
nucleon core 1 fm 9.5e+03 MeV 5.2 MeV
minimizing confinement + Coulomb energy gives hydrogen's binding:
13.61 eV -- the atomic scale from uncertainty alone
Squeezing a particle into a smaller box forces a larger momentum spread
and thus more kinetic energy (E ~ 1/dx^2). That is why electrons don't
fall into the nucleus, why atoms are ~0.1 nm (eV energies), and why
nucleons confined to femtometres carry MeV -- the energy scale of nuclei.
wrote C:\Users\acwic\symplectic-nbody\examples\output\uncertainty.svgQuantum tunneling
A particle with too little energy to climb a barrier can still leak through: its wavefunction decays as exp(-kappa x) inside, so transmission is T ~ exp(-2 kappa L), plunging exponentially with barrier width. A nanometre barrier is essentially opaque, yet shaving an Angstrom raises the current ~8x -- the razor sensitivity that lets a scanning tunneling microscope feel individual atoms, and the physics of alpha decay and fusion.
Quantum tunneling: T ~ exp(-2 kappa L), kappa = sqrt(2m(V-E))/hbar
V - E (eV) width (nm) transmission
----------------------------------------
1 0.2 1.29e-01
1 0.5 5.96e-03
1 1.0 3.55e-05
4 0.2 1.66e-02
4 0.5 3.55e-05
4 1.0 1.26e-09
STM tip-surface gap sensitivity (4 eV work function):
+0.1 nm gap -> current x 1.29e-01 (1/8)
+0.2 nm gap -> current x 1.66e-02 (1/60)
+0.3 nm gap -> current x 2.14e-03 (1/468)
Transmission plunges exponentially with width, so a barrier a nanometre
thick is essentially opaque -- yet shave off an Angstrom and the current
jumps ~8x. That razor sensitivity is how a scanning tunneling microscope
feels individual atoms, and the same barrier penetration drives alpha
decay (tunneling out) and stellar fusion (tunneling in).
wrote C:\Users\acwic\symplectic-nbody\examples\output\tunneling.svgThe particle in a box
Trap a particle in a well and only standing waves fit, quantizing the energy: E_n = n^2 h^2 / (8 m L^2). Levels rise as n^2, the ground state is nonzero (confinement zero-point energy), and every level scales as 1/L^2 -- which is why shrinking a quantum dot widens its gaps and shifts its glow bluer, giving size-tunable colour for displays and bio-markers.
Particle in a box: E_n = n^2 h^2 / (8 m L^2) (electron, L = 1 nm)
n E_n (eV) gap to n-1 (eV)
----------------------------------
1 0.376 0.000
2 1.504 1.128
3 3.384 1.880
4 6.016 2.632
5 9.401 3.384
quantum-dot n=1->2 emission (smaller box = bluer):
5 nm dot -> 27477 nm
3 nm dot -> 9892 nm
2 nm dot -> 4396 nm
1 nm dot -> 1099 nm
Only whole numbers of half-wavelengths fit between the walls, so energy
comes in n^2 rungs with a nonzero ground state (confinement zero-point
energy). Because every level scales as 1/L^2, shrinking a quantum dot
widens the gaps and shifts its glow toward the blue -- size-tunable colour,
used in displays and biological markers.
wrote C:\Users\acwic\symplectic-nbody\examples\output\particle_box.svgThe quantum harmonic oscillator
Near any potential minimum a system is a spring, so the oscillator is everywhere -- molecular vibrations, phonons, cavity photons. Its levels are EVENLY spaced by hbar omega (unlike the box or atom), so a molecule absorbs one sharp infrared line per vibrational quantum (CO at 4.6 um). The ground state is nonzero: the zero-point energy (1/2) hbar omega is forced by uncertainty and keeps helium liquid at absolute zero.
Quantum harmonic oscillator: E_n = (n + 1/2) hbar omega
molecule k (N/m) hbar omega (eV) IR wavelength
------------------------------------------------------
H2 570 0.5454 2.27 um
CO 1902 0.2690 4.61 um
N2 2294 0.2924 4.24 um
HCl 516 0.3721 3.33 um
Unlike the box (n^2) or atom (-1/n^2), the oscillator's levels are EVENLY
spaced by hbar omega, so a molecule absorbs a single sharp infrared line
per vibrational quantum. The ground state is not zero -- the zero-point
energy (1/2) hbar omega is forced by the uncertainty principle, and it is
real: it keeps helium liquid at absolute zero and shifts bond energies.
wrote C:\Users\acwic\symplectic-nbody\examples\output\harmonic_oscillator.svgRutherford scattering
Firing alphas at gold foil, a few bounced almost straight back -- impossible off diffuse charge. Rutherford's Coulomb cross section dsigma/dOmega ~ 1/sin^4(theta/2) soars at small angles but stays nonzero at 180 degrees, exactly those hard bounces, revealing a tiny dense nucleus. The head-on closest approach (~45 fm for 5 MeV alphas on gold) bounded the nuclear size.
Rutherford scattering: dsigma/dOmega ~ 1/sin^4(theta/2)
5 MeV alpha on gold: head-on closest approach = 45.5 fm
angle dsigma/dOmega (rel) impact b (fm)
----------------------------------------------
10d 4.33e+03 260.1
30d 55.7 84.9
60d 4 39.4
90d 1 22.8
120d 0.444 13.1
150d 0.287 6.1
179d 0.25 0.2
The cross section soars at small angles (grazing passes barely deflect)
and falls toward back-scattering -- but stays NONZERO at 180 degrees. Those
rare hard bounces are impossible off diffuse charge; they proved the atom's
positive charge sits in a tiny dense nucleus. The head-on approach distance
put an upper bound of tens of femtometres on the nuclear size.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rutherford.svgRadioactive decay & decay chains
Unstable nuclei decay at a constant per-nucleus rate, so a population falls exponentially N = N0 2^(-t/t_half) -- the clock behind carbon-14 dating (25% remaining is two half-lives, ~11,500 yr). In a parent-daughter chain the daughter follows the Bateman rise-and-fall, reaching secular equilibrium where its activity equals the parent's -- the principle of medical radioisotope generators and uranium-fed radon.
Radioactive decay: N = N0 2^(-t/t_half); age = t_half log2(N0/N)
carbon-14 dating (t_half = 5730 yr):
% remaining half-lives age (yr)
----------------------------------------
90% 0.15 871
50% 1.00 5730
25% 2.00 11460
10% 3.32 19035
1% 6.64 38069
Parent -> daughter chain (parent 8 d, daughter 0.6 d):
time (d) parent daughter
0 1000 0
1 917 49
2 841 60
5 648 52
10 420 34
20 177 14
The daughter starts at zero, builds up as the parent feeds it, and once
it is decaying as fast as it is produced it tracks the parent -- secular
equilibrium, where daughter activity equals parent activity. That balance
runs medical radioisotope generators and the radon from uranium in rock.
wrote C:\Users\acwic\symplectic-nbody\examples\output\radioactive_decay.svgNuclear binding: the mass formula
Weizsacker's liquid-drop model sums volume, surface, Coulomb, asymmetry and pairing terms into the nuclear binding energy. Their balance gives the binding-energy-per-nucleon curve, peaking near iron at ~8.8 MeV/nucleon -- which is exactly why fusion releases energy up to iron and fission beyond it. Minimizing over Z traces the valley of stability, drifting to neutron excess in heavy nuclei (U-238 sits at Z=92).
Semi-empirical mass formula: B/A peaks at iron (~8.8 MeV/nucleon)
nucleus Z A B/A (MeV)
----------------------------------
He-4 2 4 5.710
C-12 6 12 7.468
O-16 8 16 7.873
Fe-56 26 56 8.846
Ni-62 28 62 8.863
Kr-84 36 84 8.781
Sn-120 50 120 8.548
Pb-208 82 208 7.857
U-238 92 238 7.625
peak of the curve: A = 58, 8.86 MeV/nucleon
Light nuclei fuse toward the peak, releasing energy; heavy nuclei fission
toward it, also releasing energy. Iron-56 sits at the summit -- the ash of
stellar fusion and the end of the line for energy release, which is why a
massive star's iron core cannot burn and collapses into a supernova.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mass_formula.svgNuclear Q-value: E = mc^2 at work
A nuclear reaction releases energy equal to its mass defect times c^2 (1 amu = 931.5 MeV). D-T fusion yields 17.6 MeV, U-235 fission ~200 MeV -- only a fraction of a percent of the mass, but c^2 makes it millions of times a chemical bond: fission is ~2 million times TNT, fusion ~4x fission, and total matter-antimatter annihilation converts 100% of the mass at ~9x10^16 J/kg.
Nuclear Q = (m_reactants - m_products) c^2 (1 amu = 931.494 MeV)
reaction Q (MeV) mass converted
--------------------------------------------------
D + T -> He4 + n 17.59 0.375 %
D + D -> He3 + n 3.26 0.087 %
p-p chain (net) 26.70 0.711 %
U-235 fission 197.01 0.090 %
energy density of fuels (joules per kilogram):
TNT (chemical): 4.60e+06 J/kg
gasoline: 4.60e+07 J/kg
U-235 fission: 8.05e+13 J/kg
D-T fusion: 3.38e+14 J/kg
matter-antimatter: 8.99e+16 J/kg
A nuclear reaction converts only a fraction of a percent of its mass, but
c^2 makes that millions of times more than any chemical bond -- fission
~2 million times TNT, fusion ~4x fission. Total annihilation converts 100%
of the mass, the ultimate energy density c^2 ~ 9x10^16 J/kg.
wrote C:\Users\acwic\symplectic-nbody\examples\output\q_value.svgQuantum statistics
Identical particles come in two kinds. Fermions obey Pauli exclusion, so their Fermi-Dirac occupation never exceeds one and at T=0 fills states in a sharp step up to the Fermi level -- electron degeneracy, white-dwarf pressure. Bosons pile up without limit (Bose-Einstein), condensing into the ground state below a critical temperature. Far above the chemical potential both fade into the classical Maxwell-Boltzmann exponential.
Quantum statistics: occupation <n> vs (E-mu)/kT
(E-mu)/kT Fermi-Dirac Bose-Einstein Maxwell-Boltz
----------------------------------------------------------
-4.0 0.9820 inf 54.5982
-1.0 0.7311 inf 2.7183
0.5 0.3775 1.541 0.6065
1.0 0.2689 0.582 0.3679
2.0 0.1192 0.157 0.1353
4.0 0.0180 0.019 0.0183
8.0 0.0003 0.000 0.0003
Fermions can never exceed one per state (Pauli), so at low temperature
they stack into a sharp step up to the Fermi level -- electron degeneracy,
white-dwarf pressure. Bosons pile up without limit as E -> mu, condensing
into the ground state below a critical temperature (BEC, superfluid He).
Far above mu both fade into the classical Maxwell-Boltzmann exponential.
wrote C:\Users\acwic\symplectic-nbody\examples\output\quantum_stats.svgDebye specific heat
Classically a solid stores 3R of heat per mole (Dulong-Petit), but measured heat capacities plunge toward zero in the cold. Debye treated the vibrations as quantized phonons with a maximum frequency (the Debye temperature), giving C_V ~ T^3 at low T and 3R at high T. A stiff light lattice like diamond (Theta_D ~ 2230 K) is still 'cold' at room temperature -- only ~1/6 of 3R -- while soft heavy lead has long reached the plateau.
Debye model: C_V rises as T^3, plateaus at Dulong-Petit 3R = 24.94 J/mol/K
material Theta_D (K) C_V @300K % of 3R
------------------------------------------------
lead 105 24.79 99.4%
copper 343 23.39 93.8%
aluminium 428 22.58 90.5%
diamond 2230 4.13 16.6%
Every solid follows one universal curve in T/Theta_D. A stiff, light
lattice has a high Debye temperature, so at room temperature it is still
'cold' -- diamond stores only ~1/6 of its classical heat capacity, while
soft heavy lead has long since reached the 3R plateau. The T^3 falloff at
low temperature is the fingerprint of phonon quantization.
wrote C:\Users\acwic\symplectic-nbody\examples\output\debye_heat.svgThe Carnot cycle
No heat engine between reservoirs at T_h and T_c can beat eta = 1 - T_c/T_h -- the second law forces some heat to be dumped to the cold side. A steam plant at 800 K exhausting to 300 K is capped at 62%; ocean-thermal at a 20 K gap only 7%. Reversed, the cycle is a heat pump with COP >> 1, delivering many times the heat of the work it draws -- why heat pumps beat resistive heaters.
Carnot: no engine beats eta = 1 - T_c/T_h
engine T_hot (K) T_cold (K) max eta
-------------------------------------------------------
car engine 2000 300 0.850
steam power plant 800 300 0.625
geothermal 450 300 0.333
ocean thermal (OTEC) 298 278 0.067
running the cycle backwards (T_hot=293 K room, T_cold=273 K):
refrigerator COP = 13.7 (heat removed per unit work)
heat pump COP = 14.7 (heat delivered per unit work)
Some heat must always be dumped to the cold reservoir, so no engine hits
100% -- a steam plant at 800 K exhausting to 300 K is capped at 62%, and
real losses cut it to ~40%. Reversed, the same cycle is a heat pump that
delivers many times the heat of the work it draws (COP >> 1), which is why
heat pumps beat resistive heaters -- they move heat rather than make it.
wrote C:\Users\acwic\symplectic-nbody\examples\output\carnot.svgAdiabatic processes
Compress or expand a gas with no time to shed heat and it obeys P V^gamma = const, T V^(gamma-1) = const -- and its temperature changes. A diesel engine's 22:1 squeeze reaches ~1000 K and ignites fuel without a spark; expansion cools (rising air, released spray). Sound waves compress air adiabatically, so the speed of sound carries Laplace's sqrt(gamma) factor -- the fix that corrected Newton's ~18% error.
Adiabatic: P V^gamma = const, T V^(gamma-1) = const (no heat flow)
speed of sound in air (293 K): 343.2 m/s
(Laplace's gamma vs Newton's wrong 290 m/s)
adiabatic compression of air from 300 K:
ratio V1/V2 T2 (K) P2/P1
----------------------------------
2 396 2.6
5 571 9.5
10 754 25.1
22 1033 75.8
50 1435 239.1
Compressing a gas with no time to shed heat drives its temperature up
steeply -- a diesel engine's 22:1 squeeze reaches ~1000 K and ignites fuel
without a spark. Expansion does the reverse, cooling the gas (rising air,
released CO2). Sound waves compress air adiabatically too, so the speed of
sound carries Laplace's sqrt(gamma) factor -- the fix to Newton's estimate.
wrote C:\Users\acwic\symplectic-nbody\examples\output\adiabatic.svgThe van der Waals gas
Give the ideal gas molecules a finite size (b) and mutual attraction (a) and it can condense: (P + a n^2/V^2)(V - nb) = nRT. Below the critical temperature the isotherm develops an unstable loop where gas turns to liquid. The critical constants follow from a and b alone -- CO2's 304 K, 7.4 MPa -- and the compressibility Pc Vc / R Tc = 3/8 is universal, the law of corresponding states.
Van der Waals: (P + a n^2/V^2)(V - nb) = nRT -- a gas that can condense
gas T_c (K) P_c (MPa) Pc Vc / R Tc
------------------------------------------------
helium 5.2 0.23 0.3750
CO2 304.0 7.40 0.3750
water 647.0 22.06 0.3750
The finite-size (b) and attraction (a) terms give the ideal gas something
it lacks: a liquid-vapour transition. Above T_c the isotherm is smooth, but
below it a wiggle appears -- pressure would rise with volume, which is
unstable, so the gas condenses across it. At the critical point that wiggle
is an inflection, and Pc Vc / R Tc = 3/8 for EVERY van der Waals gas.
wrote C:\Users\acwic\symplectic-nbody\examples\output\van_der_waals.svgThe Joule-Thomson effect
Push a real gas through a valve and it cools or warms depending on which wins: attraction (cools) or finite molecular size (warms). Below the inversion temperature T_inv = (27/4) T_c throttling cools -- so nitrogen and CO2 liquefy by repeated expansion at room temperature, but hydrogen and helium (low T_inv) warm and must be pre-cooled first. An ideal gas has no Joule-Thomson effect at all.
Joule-Thomson: throttling cools below T_inv, warms above
gas T_inv (K) at 300 K mu (K/MPa) @300K
----------------------------------------------------
CO2 2052 cools 6.717
N2 852 cools 2.445
H2 224 warms -0.235
He 35 warms -1.011
Two effects compete when a real gas expands through a valve: attraction
cools it (work pulling molecules apart), finite size warms it. Below the
inversion temperature attraction wins, so nitrogen and CO2 cool at room
temperature and can be liquefied by repeated throttling. Hydrogen and
helium have low inversion temperatures, so they must be pre-cooled first
-- otherwise throttling heats them, an early liquefaction hazard.
wrote C:\Users\acwic\symplectic-nbody\examples\output\joule_thomson.svgClausius-Clapeyron: vapor pressure
Along a liquid-vapour line, pressure and temperature are locked by dP/dT = L/(T dV), integrating to P(T) = P0 exp(-(L/R)(1/T - 1/T0)) -- vapor pressure climbs exponentially, roughly doubling every ~15 K. Boiling is where it meets the ambient pressure, so water boils at 72 C atop Everest (thin air) and 121 C in a pressure cooker. The same curve sets atmospheric humidity and cloud formation.
Clausius-Clapeyron: P(T) = P0 exp(-(L/R)(1/T - 1/T0))
water boiling point vs altitude:
location altitude (m) pressure (kPa) boils at (C)
--------------------------------------------------------------
sea level 0 101.3 100.0
Denver 1609 83.7 94.6
La Paz 3640 65.7 88.1
Everest base 5364 53.5 82.7
Everest summit 8848 35.3 72.2
Vapor pressure climbs exponentially with temperature -- roughly doubling
every ~15 K -- so water boils when its vapor pressure reaches the ambient
air pressure. Up a mountain the thinner air lets it boil cooler (72 C on
Everest, too cold to cook an egg), while a pressure cooker raises the
boiling point to ~121 C. The same curve sets humidity and cloud formation.
wrote C:\Users\acwic\symplectic-nbody\examples\output\clausius_clapeyron.svgThe Reynolds number
One dimensionless ratio Re = rho v L / mu decides whether a flow is smooth or turbulent: viscosity damps disturbances at low Re, inertia tears them into eddies at high Re, with pipe flow transitioning near Re ~ 2300. It spans 13 orders of magnitude -- a bacterium at Re ~ 1e-5 lives in pure viscosity and cannot coast, a whale at Re ~ 1e8 glides on inertia. Laminar flow follows Hagen-Poiseuille's r^4 law.
Reynolds number Re = rho v L / mu: inertia vs viscosity
system v (m/s) L (m) Re regime
----------------------------------------------------------------
bacterium 3e-05 1e-06 3.0e-05 laminar
sperm cell 0.0002 5e-05 1.0e-02 laminar
blood in capillary 0.001 8e-06 8.0e-03 laminar
water tap (pipe) 1 0.02 2.0e+04 turbulent
swimming human 1.5 1.8 2.7e+06 turbulent
airliner wing 2.5e+02 3 5.1e+07 turbulent
blue whale 10 25 2.5e+08 turbulent
Thirteen orders of magnitude of Re separate a bacterium from a whale.
At tiny Re viscosity rules -- a microbe cannot coast, it is like swimming
in honey. At huge Re inertia rules and the flow tumbles into turbulence.
For pipe flow the crossover sits near Re ~ 2300, which is why a slow tap
runs in smooth laminar sheets but a fast one roars and sputters.
wrote C:\Users\acwic\symplectic-nbody\examples\output\reynolds.svgBernoulli & the Venturi effect
Along a streamline P + 1/2 rho v^2 + rho g h is constant, so speeding up a flow drops its pressure. A narrowing Venturi pipe is fastest and lowest-pressure at the throat -- drawing fuel into a carburettor, reading flow in a meter, and (with circulation) helping lift a wing. A Pitot tube runs it backwards to give airspeed, and Torricelli's sqrt(2gh) jet is the same law with the pressures cancelled.
Bernoulli: P + 1/2 rho v^2 + rho g h = const -- fast flow, low pressure
Pitot airspeed from dynamic pressure (air):
true airspeed dynamic P (kPa)
50 m/s 1.53
100 m/s 6.13
250 m/s 38.28
Torricelli efflux (water jet from a hole at depth h):
depth 1 m -> 4.4 m/s
depth 5 m -> 9.9 m/s
depth 20 m -> 19.8 m/s
A narrowing pipe (Venturi) speeds the flow -- so by Bernoulli its pressure
drops right where it is fastest. That suction draws fuel into a carburettor,
lets a Venturi meter read flow from a pressure gap, and, with circulation,
helps hold an airfoil up. A Pitot tube runs it backwards, stopping the flow
to turn its speed into a pressure an aircraft reads as airspeed.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bernoulli.svgSurface tension: capillary rise & Laplace pressure
A liquid surface costs energy per unit area (gamma), so it behaves like a stretched skin. In a thin tube that pull lifts water against gravity by Jurin's law h = 2 gamma cos(theta) / (rho g r) -- a 1 mm bore climbs ~1.5 cm, a 1 micron root pore tens of metres. A curved surface also holds a pressure jump 2 gamma/r (a droplet) or 4 gamma/r (a soap bubble's two films), so smaller drops run at higher pressure and empty into larger ones.
Surface tension: gamma = 0.0728 N/m for water at 20 C
Capillary rise (Jurin's law h = 2 gamma cos(theta) / (rho g r)):
tube radius rise
5.000 mm 2.98 mm
1.000 mm 1.49 cm
0.500 mm 2.98 cm
0.100 mm 14.88 cm
0.010 mm 1.5 m
0.001 mm 14.9 m
Young-Laplace overpressure (droplet 2 gamma/r, bubble 4 gamma/r):
radius droplet soap bubble
5.0 mm 29.1 Pa 58.2 Pa
1.0 mm 145.6 Pa 291.2 Pa
0.1 mm 1456.0 Pa 2912.0 Pa
Narrower bore -> higher climb (rise ~ 1/r): a 1 micron root pore lifts
water tens of metres. Smaller drops hold higher pressure, so when a small
bubble meets a big one through a tube, the small one empties into the big.
wrote C:\Users\acwic\symplectic-nbody\examples\output\surface_tension.svgThe Ekman spiral: wind, rotation & the ocean
Steady wind drags the sea surface, but Coriolis deflects the current 45 degrees to the right of the wind (northern hemisphere). Balancing friction against rotation, the current spirals clockwise and decays exponentially with depth over the Ekman depth D = pi sqrt(2 A_z/|f|). The vertically integrated transport ends up exactly 90 degrees to the right of the wind with magnitude tau/(rho |f|), independent of viscosity -- the sideways pumping behind coastal upwelling and the ocean gyres.
Ekman spiral: wind-driven ocean boundary layer at 45 N
eddy viscosity A_z = 0.05 m^2/s wind stress tau = 0.10 Pa
Coriolis f = 1.031e-04 /s Ekman depth D = 97.8 m
Current spirals CLOCKWISE, decaying with depth (wind blows toward +x/east):
depth speed direction
-0.0 m 4.30 cm/s -45 deg from wind
-9.8 m 3.14 cm/s -63 deg from wind
-24.5 m 1.96 cm/s -90 deg from wind
-48.9 m 0.89 cm/s -135 deg from wind
-73.4 m 0.41 cm/s -180 deg from wind
-97.8 m 0.19 cm/s 135 deg from wind
Net Ekman transport = 0.946 m^2/s, directed 90 deg to the RIGHT of the wind.
Surface current sits 45 deg right of the wind; each deeper layer is dragged
further right and weaker, so the vertical sum points fully across the wind --
the sideways pumping that drives coastal upwelling and the ocean gyres.
wrote C:\Users\acwic\symplectic-nbody\examples\output\ekman.svgMilankovitch cycles: orbits and the ice ages
Slow changes in Earth's orbit -- eccentricity (~100 kyr), obliquity (~41 kyr) and precession (~23 kyr) -- redistribute sunlight between seasons and latitudes even though the annual total barely moves. Computing daily top-of-atmosphere insolation from the astronomical formula reproduces the ~478 W/m^2 65N midsummer peak; that high-latitude summer sun is the knob that decides whether winter snow survives to build ice sheets, so cool summers (low tilt, summer at aphelion) grow the glaciers.
Milankovitch: 65N midsummer insolation drives ice sheets (grow when it's weak) Present orbit: e=0.0167, obliquity=23.44 deg, perihelion in NH winter orbital state 65N June (W/m^2) present day 477.8 (+0.0) low obliquity 22.1 deg (cool summers) 456.5 (-21.4) high obliquity 24.5 deg (warm summers) 495.7 (+17.8) high ecc 0.05, summer at aphelion 446.6 (-31.2) high ecc 0.05, summer at perihelion 545.6 (+67.8) Cool NH summers (low obliquity + summer at aphelion) let winter snow survive and ice sheets grow -- the ~41 kyr tilt and ~23 kyr precession beats, modulated by the ~100 kyr eccentricity envelope, that pace the Pleistocene glacial cycles. wrote C:\Users\acwic\symplectic-nbody\examples\output\milankovitch.svg
Equipartition & the heat-capacity staircase
Classically every quadratic degree of freedom carries (1/2) k_B T, so an ideal gas has C_V = (f/2)R and gamma = (f+2)/f: 3R/2 and 5/3 for a monatomic gas, 5R/2 and 7/5 for a diatomic at room temperature, and 3R for a solid (Dulong-Petit). But equipartition is only the hot-limit ceiling -- quantum mechanics freezes a mode out below its energy quantum, so H2 climbs a staircase from 3R/2 to 5R/2 (rotation thaws near 100 K) toward 7R/2 (vibration near 1000s K).
Equipartition: (1/2) k_B T per quadratic degree of freedom
gas / solid f C_V/R C_P/R gamma
monatomic (He, Ar) 3 1.50 2.50 1.667
diatomic, room T (N2) 5 2.50 3.50 1.400
diatomic, hot (+vib) 7 3.50 4.50 1.286
nonlinear triatomic 6 3.00 4.00 1.333
solid (Dulong-Petit) 6 3.00 4.00 1.333
H2 heat-capacity staircase (theta_rot=85 K, theta_vib=6000 K):
T (K) C_V/R
20 1.77
50 2.29
100 2.44
300 2.49
1000 2.59
3000 3.22
6000 3.42
10000 3.47
Classical equipartition is the high-T ceiling; each mode contributes only
once k_B T tops its quantum. So H2 sits at 3R/2 while rotation is frozen, steps
up to 5R/2 once it thaws (~100 K), and only near ~1000s K does vibration lift it
toward 7R/2 -- the staircase Maxwell could see but not explain before quanta.
wrote C:\Users\acwic\symplectic-nbody\examples\output\equipartition.svgOsmotic pressure: van't Hoff across a membrane
Water crosses a semipermeable membrane into a solution until the built-up head balances it. For dilute solutions the equilibrium pressure follows van't Hoff's law Pi = i c R T -- the ideal-gas law with solute particles as the gas, so a salt that splits into i ions pushes i times as hard. It reproduces seawater's ~27 atm (the wall reverse-osmosis desalination must beat) and blood plasma's ~7.6 atm (which sets isotonic IV fluids), and it weighs macromolecules by the tiny pressure they raise.
Osmotic pressure: Pi = i c R T (van't Hoff -- the ideal-gas law for solutes) solution conc i Pi (atm) glucose, 0.30 mol/L (isotonic) 0.30 M 1 7.63 blood plasma (~0.30 osmol/L) 0.30 M 1 7.63 normal saline 0.9% (~0.15 M NaCl) 0.15 M 2 7.63 seawater (~0.6 M NaCl-equiv) 0.60 M 2 28.36 Dead Sea brine (~5 M ions) 2.50 M 2 122.27 Same molarity, more particles -> more pressure: CaCl2 (i~3) pushes three times as hard as glucose (i=1). Seawater's ~27 atm is the wall reverse-osmosis must beat to squeeze fresh water back out; blood's ~7.6 atm sets isotonic IV fluids. wrote C:\Users\acwic\symplectic-nbody\examples\output\osmosis.svg
Fick diffusion: spreading as the root of time
A random walk spreads a concentration downhill: Fick's first law J = -D dC/dx and the diffusion equation dC/dt = D d^2C/dx^2. A point release stays a Gaussian whose rms width grows as sqrt(2 D t) -- the diffusive sqrt(t), never the ballistic t -- and a step interface relaxes through an error-function profile. Because the time to cross a length scales as L^2/D, diffusion is fast across a cell (~0.1 s) but hopeless across a room (~30 years), which is why life is small and large systems need flow. Stokes-Einstein ties D to temperature and drag.
Fick diffusion: a random walk spreads as sqrt(t), not t
Small molecule in water, D = 1e-9 m^2/s
distance diffusion time
1 um (organelle) 1.0 ms
10 um (cell) 100.0 ms
1 mm (tissue) 1000.00 s
1 cm 27.8 hr
1 m (room) 32 yr
Spread of a point release (sigma = sqrt(2 D t)):
t = 1 s -> sigma = 0.045 mm
t = 100 s -> sigma = 0.447 mm
t = 10000 s -> sigma = 4.472 mm
Stokes-Einstein for a 1 nm sphere in water at 25 C: D = 2.18e-10 m^2/s
Diffusion is quick across a cell but takes ~30 years across a room, since time
scales as L^2/D -- why microscopic life relies on it and large bodies need flow.
wrote C:\Users\acwic\symplectic-nbody\examples\output\diffusion.svgThe Peclet number: carried vs spreading
Heat and solutes move both by being carried in the flow (advection) and by spreading down their gradient (diffusion); the Peclet number Pe = U L / D says which wins. Pe << 1 (a cell, a still cup) is diffusion-controlled; Pe >> 1 (a river, an artery) is swept along in thin plumes. It factors as Pe = Re*Pr for heat and Re*Sc for mass, so water's Pr ~ 7, air's Pr ~ 0.7 and an aqueous solute's Sc ~ 1000 set boundary-layer thicknesses, and the crossover Pe = 1 sits at the length L = D/U.
Peclet number Pe = U L / D: advection (carried) vs diffusion (spreads)
system U (m/s) L (m) Pe regime
inside a cell 1.0e-07 1.0e-05 1.00e-03 diffusion
blood in a capillary 5.0e-04 8.0e-06 4.00e+00 advection
slowly stirred cup 5.0e-02 5.0e-02 2.50e+06 advection
stream / small river 5.0e-01 1.0e+00 5.00e+08 advection
Fluid diffusivity ratios:
water: Pr = nu/alpha = 7.0 Sc = nu/D = 1000 Le = alpha/D = 144
air: Pr = 0.70 (heat and momentum spread alike)
Pe = Re*Pr for heat and Re*Sc for mass. The crossover Pe=1 sits at L = D/U:
U = 1e-06 m/s -> crossover length = 1.00e-03 m
U = 1e-03 m/s -> crossover length = 1.00e-06 m
U = 1e+00 m/s -> crossover length = 1.00e-09 m
Below that length diffusion wins; above it the flow carries faster than spreading.
wrote C:\Users\acwic\symplectic-nbody\examples\output\peclet.svgConvective heat transfer & Newton cooling
A moving fluid strips heat off a surface at q = h (T_s - T_inf), and the coefficient h follows from the Nusselt number Nu = h L / k -- the ratio of convective to conductive transport. Correlations give it: Dittus-Boelter Nu = 0.023 Re^0.8 Pr^0.4 for turbulent pipe flow, 0.664 Re^0.5 Pr^(1/3) for a laminar plate. A lumped object then cools exponentially with tau = rho c_p V/(h A) provided the Biot number Bi = h L/k_solid stays below ~0.1, so an aluminium block cools in an hour in still air but seconds in forced water.
Convective heat transfer: q = h (T_s - T_inf), Nu = h L / k
Forced water in a pipe (Dittus-Boelter Nu = 0.023 Re^0.8 Pr^0.4):
Re=2e4, Pr=7 -> Nu = 138, h = 4147 W/(m^2 K)
1 cm aluminium cube cooling from 100 C in 20 C surroundings:
regime h (W/m^2K) Biot tau t to 30 C
still air 8 0.0002 8.4 min 17.5 min
breeze / fan 40 0.0010 1.7 min 3.5 min
forced water 2000 0.0488 2.0 s 4.2 s
All three stay near-isothermal inside (Bi << 1), so cooling is a clean
exponential T(t) = T_inf + (T0 - T_inf) e^(-t/tau). Stiffer convection = bigger h
= shorter tau: still air takes ~an hour, forced water seconds. Newton's law again.
wrote C:\Users\acwic\symplectic-nbody\examples\output\convection.svgThe Stefan problem: a freezing front as sqrt(t)
A melting or freezing interface is driven by latent heat, not temperature alone: the heat released as water freezes must conduct out through the ice already formed, so the front advances as X = 2 lambda sqrt(alpha t), slowing as it deepens. The growth coefficient lambda solves the Stefan condition lambda e^(lambda^2) erf(lambda) = St/sqrt(pi), where the Stefan number St = c_p dT/L weighs sensible against latent heat. It reproduces Stefan's classic estimate -- about 10 cm of ice after a day of hard frost -- and the depth^2 time law that makes the next foot take weeks.
Stefan problem: freezing front X(t) = 2 lambda sqrt(alpha t)
ice: alpha = 1.14e-06 m^2/s, latent heat L = 334 kJ/kg
frost St lambda ice @ 1 day ice @ 1 wk
light frost (-5 C) 0.031 0.125 7.8 cm 20.7 cm
hard frost (-15 C) 0.094 0.214 13.4 cm 35.6 cm
arctic (-40 C) 0.251 0.341 21.4 cm 56.7 cm
To reach a given thickness (hard frost, -15 C):
5 cm -> 0.1 days
10 cm -> 0.6 days
30 cm -> 5.0 days
50 cm -> 13.8 days
The front slows as sqrt(t): the latent heat released at the interface must
conduct out through the ice already formed, and that layer thickens, so the
first few cm come in hours but the next foot takes weeks. Same law crusts lava.
wrote C:\Users\acwic\symplectic-nbody\examples\output\stefan.svgThe capillary length: surface tension vs gravity
The same liquid makes a round dewdrop and a flat puddle -- the difference is size. Surface tension pulls toward a sphere, gravity flattens anything taller than the capillary length l_c = sqrt(gamma/(rho g)) (~2.7 mm for water). The Bond number Bo = (L/l_c)^2 says which wins: below 1 drops are round, above 1 they puddle out (capped at ~2 l_c deep). A moving drop adds inertia through the Weber number and shatters once We tops ~12, and a thin jet pinches into drops spaced ~9 radii apart by the Rayleigh-Plateau instability.
Capillary length l_c = sqrt(gamma / (rho g)): surface tension vs gravity liquid gamma (N/m) rho l_c (mm) water 0.0728 998 2.73 mercury 0.4870 13534 1.92 ethanol 0.0223 789 1.70 liquid nitrogen 0.0089 807 1.06 Water crossover l_c = 2.73 mm. Bond number Bo = (L/l_c)^2: drop / feature size Bo regime mist droplet 0.02mm 0.00 round (tension) raindrop 2.00mm 0.54 round (tension) water strider foot dimple 3.00mm 1.21 flat (gravity) coin of water 10.00mm 13.44 flat (gravity) spilled puddle 50.00mm 336.09 flat (gravity) Max non-wetting puddle depth = 2 l_c = 5.5 mm (water can't pile higher). A moving 2 mm drop shatters once it tops the critical Weber number, at v = 0.66 m/s -- why rain fragments and sprays atomize. wrote C:\Users\acwic\symplectic-nbody\examples\output\capillary.svg
The Froude number: racing your own waves
A surface disturbance travels at the shallow-water wave speed sqrt(g h), and the Froude number Fr = U/sqrt(g h) compares the flow to it. Fr < 1 is tranquil (subcritical) flow whose ripples run upstream; Fr > 1 is shooting (supercritical) flow that outruns its waves, so a sudden slowing throws up a hydraulic jump. For a ship the hull Froude number Fr = U/sqrt(g L) sets wave-making drag, walling a displacement hull near Fr ~ 0.4 (the 1.34 sqrt(L_ft) knots rule), and its wake wedge holds a fixed 19.47-degree half-angle at any speed.
Froude number Fr = U / sqrt(g h): flow speed vs its own wave speed flow U (m/s) h (m) Fr regime lazy river 0.50 2.000 0.11 subcritical brisk stream 1.50 0.500 0.68 subcritical below a spillway 6.00 0.200 4.28 supercritical kitchen-sink disc 1.00 0.001 10.10 supercritical Hull speed (displacement wall at Fr ~ 0.40): boat / waterline L (m) V_hull (m/s) knots kayak 4.0 2.52 4.9 day-sailer 7.0 3.34 6.5 yacht 12.0 4.37 8.5 clipper 60.0 9.77 19.0 Kelvin ship-wake half-angle: 19.47 deg -- fixed, whatever the speed. A hydraulic jump: supercritical Fr1=3 water 0.2 m deep leaps to 0.75 m and goes subcritical, dumping its excess energy into turbulence -- the step below a weir. wrote C:\Users\acwic\symplectic-nbody\examples\output\froude.svg
The Mach cone: the geometry of going supersonic
When a source outruns sound, its wavelets pile into a trailing cone whose half-angle obeys sin(mu) = 1/M -- 90 degrees at Mach 1, tightening to 30 at Mach 2 and 11.5 at Mach 5. That cone is the shock a ground observer hears as a sonic boom, laid down behind the overhead point and sweeping a continuous carpet along the track. Below Mach 1, thin-airfoil lift diverges by the Prandtl-Glauert factor 1/sqrt(1-M^2) toward the sound barrier; above it, a flow turning a corner expands through the Prandtl-Meyer angle.
Mach cone: sin(mu) = 1/M -- faster flight, tighter cone
Mach cone half-angle boom lag @ 12 km
1.0 90.00 deg 0.0 s
1.2 56.44 deg 22.5 s
2.0 30.00 deg 35.2 s
3.0 19.47 deg 38.4 s
5.0 11.54 deg 39.9 s
Subsonic compressibility (Prandtl-Glauert lift factor 1/sqrt(1-M^2)):
M = 0.30 -> x1.05
M = 0.60 -> x1.25
M = 0.80 -> x1.67
M = 0.90 -> x2.29
M = 0.95 -> x3.20
Supersonic turning (Prandtl-Meyer angle, gamma=1.4):
M = 1.5 -> nu = 11.9 deg
M = 2.0 -> nu = 26.4 deg
M = 3.0 -> nu = 49.8 deg
M = 5.0 -> nu = 76.9 deg
At Mach 2 and 12 km, the boom carpet lands 20.8 km behind the overhead point,
reaching the ground seconds after the jet has already gone by -- and because the
cone trails at a fixed angle, it sweeps a continuous 'boom carpet' along the track.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mach_cone.svgThe de Laval nozzle: making exhaust supersonic
Subsonic flow speeds up as a pipe narrows, but supersonic flow speeds up as it widens -- so to push exhaust past Mach 1 you squeeze the gas to a sonic throat and then expand it through a diverging bell. The isentropic relations fix everything from the local Mach number: the area-Mach relation A/A* is minimal at the throat, the flow chokes there once the pressure ratio drops below ~0.528 (air), and the exit Mach number is then set purely by the bell's area ratio -- 25 gives Mach 5. It is how every rocket and supersonic tunnel works.
de Laval nozzle: converge to sonic throat, diverge to supersonic exhaust
Air (gamma=1.4). Choking pressure ratio P*/P0 = 0.5283
area ratio A_e/A* exit Mach P_e/P0
1.0 1.00 0.5283
2.0 2.20 0.0939
4.0 2.94 0.0298
10.0 3.92 0.0073
25.0 5.00 0.0019
100.0 6.94 0.0003
Rocket-ish chamber: P0 = 5 MPa, T0 = 3000 K, throat A* = 10 cm^2:
choked mass flow = 3.69 kg/s
area ratio 25 -> exit Mach 5.00, exhaust 2241 m/s
Subsonic flow accelerates as area shrinks, but past Mach 1 it reverses: a
supersonic stream speeds up as the area GROWS, so the throat must sit exactly at
Mach 1. Once choked, mass flow is capped by the throat and only the bell sets M_e.
wrote C:\Users\acwic\symplectic-nbody\examples\output\nozzle.svgThe Blasius boundary layer
No-slip makes a thin sheared film cling to any surface in a stream. Blasius solved the laminar flat-plate case exactly: the 99%-thickness grows as delta = 5.0 x/sqrt(Re_x) -- only millimetres over the front of a wing -- with the displacement and momentum thicknesses tracking it at 1.721 and 0.664. The wall shear gives a local skin friction c_f = 0.664/sqrt(Re_x) (heaviest at the sharp leading edge) and a plate drag C_D = 1.328/sqrt(Re_L), and the layer stays laminar until Re_x ~ 5e5, where it trips to turbulence.
Blasius boundary layer: delta = 5.0 x / sqrt(Re_x) (air, U = 10 m/s)
x (m) Re_x delta (mm) c_f
0.01 6667 0.61 0.00813
0.05 33333 1.37 0.00364
0.10 66667 1.94 0.00257
0.30 200000 3.35 0.00148
0.75 500000 5.30 0.00094
Laminar until Re_x ~ 5e5, i.e. x = 0.75 m; beyond that it turns turbulent.
A 0.5 m x 0.2 m plate: C_D = 0.00230, friction drag = 0.0141 N per side.
The layer thickens as sqrt(x) -- a couple of mm over the front of a wing -- while
the skin friction thins as 1/sqrt(x), heaviest right at the sharp leading edge.
wrote C:\Users\acwic\symplectic-nbody\examples\output\blasius.svgThe Strouhal number: von Karman vortex streets
A blunt body in a steady flow sheds vortices alternately from each side, a staggered von Karman street whose frequency obeys St = f d / U with St ~ 0.2 nearly constant over a huge Reynolds-number range. So shedding frequency scales linearly with wind speed -- the aeolian hum of a wire (a 5 mm wire in 10 m/s wind sings at 400 Hz), the flutter of an antenna. When that frequency crosses a structure's natural frequency the flow locks in and the alternating side-force can drive destructive vortex-induced vibration, the reason chimneys wear helical strakes.
Strouhal number St = f d / U ~ 0.2: the rhythm of vortex shedding
body d wind shed freq
telephone wire 5mm 10 m/s 400.0 Hz
car antenna 8mm 25 m/s 625.0 Hz
ship rigging 20mm 15 m/s 150.0 Hz
factory chimney 3000mm 12 m/s 800.0 mHz
Roshko St = 0.212(1 - 21.2/Re) rises toward ~0.21 as Re grows:
Re = 100 -> St = 0.167
Re = 300 -> St = 0.197
Re = 1000 -> St = 0.208
Re = 10000 -> St = 0.212
Re = 1000000 -> St = 0.212
A 3 m chimney with a 0.25 Hz sway mode locks in at wind ~3.8 m/s -- when the
shedding beat hits the structure's resonance, the alternating side-force can
build destructive vortex-induced vibration (why chimneys wear helical strakes).
Wake vortices trail ~5 diameters apart.
wrote C:\Users\acwic\symplectic-nbody\examples\output\strouhal.svgWeighing clusters: the virial mass & dark matter
The virial theorem turns a cluster's own motion into a scale: M = alpha sigma^2 R/G from the velocity dispersion sigma and size R. Only the line-of-sight dispersion is observable, so sigma^2 = 3 sigma_los^2 for an isotropic system. This is Zwicky's 1933 Coma calculation -- galaxies moving at ~1000 km/s across ~1.5 Mpc demand a dynamical mass ~10^15 solar masses, about a hundred times the visible stars. The mass-to-light ratio jumps from a few for stars to hundreds for clusters: the first evidence for dark matter.
Weighing clusters by their motion: M = alpha sigma^2 R / G (virial) cluster sigma_los R (Mpc) M (Msun) M/L Virgo 700 km/s 1.5 2.56e+15 854 Coma 1000 km/s 1.5 5.23e+15 1046 Norma 900 km/s 1.3 3.67e+15 918 Bullet 1200 km/s 2.0 1.00e+16 1674 Escape velocity from Coma (1e15 Msun, 1.5 Mpc): 2395 km/s -- its galaxies at ~1000 km/s would fly apart on stellar mass alone. Zwicky (1933) found the mass needed to bind Coma was ~100x its visible stars: the first case for dark matter. With only ~5% of the mass luminous, the dark-matter fraction is ~95%. wrote C:\Users\acwic\symplectic-nbody\examples\output\cluster_mass.svg
The Sersic profile: the shape of a galaxy's light
A galaxy's surface brightness falls off as I(R) = I_e exp{-b_n[(R/R_e)^(1/n)-1]}, the Sersic law, with the index n setting the concentration. n=1 is the exponential disk of a spiral (scale length R_e/1.678); n=4 is the de Vaucouleurs law of a giant elliptical -- a bright cusped core and enormous faint wings. Half the light always sits inside the effective radius R_e (that is its definition), and integrating the profile gives the total luminosity in closed form via the gamma function, so I_e, R_e and n weigh a galaxy's stars.
Sersic profile I(R) = I_e exp{-b_n[(R/R_e)^(1/n) - 1]}
profile b_n I(0.1 R_e)/I_e R(90% light)/R_e
n=1 exponential disk 1.677 4.5 2.32
n=2 bulge 3.672 12.3 3.31
n=4 de Vaucouleurs 7.669 28.7 5.55
n=1 exponential disk: scale length h = R_e/1.678 = 0.596 R_e
Half the light sits inside R_e for every n (that's the definition), but larger n
packs a far brighter core AND flings more light into faint outer wings -- the
cuspy, extended glow of a giant elliptical versus the gentle fade of a disk.
Total luminosity L = I_e R_e^2 2 pi n e^b_n Gamma(2n)/b_n^2n (I_e=R_e=1):
n=1 exponential disk L = 11.95
n=2 bulge L = 16.31
n=4 de Vaucouleurs L = 22.67
wrote C:\Users\acwic\symplectic-nbody\examples\output\sersic.svgThe Grashof number: heat that stirs its own wind
Natural convection has no fan -- warm fluid expands, rises, and drags cooler fluid in behind it. The Grashof number Gr = g beta dT L^3/nu^2 measures that buoyant drive against viscosity, playing the role Reynolds plays in forced flow. Heat transfer correlates against the Rayleigh number Ra = Gr Pr: Nu = 0.59 Ra^(1/4) laminar, 0.10 Ra^(1/3) turbulent past Ra ~ 1e9. The result is the gentle few W/(m^2 K) of a radiator warming a still room, an order of magnitude below forced convection; Gr/Re^2 tells you which regime rules.
Grashof number Gr = g beta dT L^3 / nu^2: buoyancy-driven natural convection
Vertical wall 20 K above still air (beta=1/300, Pr=0.71):
height L Gr Ra regime h (W/m^2K)
0.05 m 3.63e+05 2.58e+05 laminar 6.91
0.30 m 7.85e+07 5.57e+07 laminar 4.42
1.00 m 2.91e+09 2.06e+09 turbulent 3.31
3.00 m 7.85e+10 5.57e+10 turbulent 3.31
10.00 m 2.91e+12 2.06e+12 turbulent 3.31
Buoyancy alone gives only a few W/(m^2 K) -- the gentle warmth off a radiator
or a sun-baked wall, an order of magnitude below a fan's forced convection. The
layer stays laminar until Ra ~ 1e9 (around a metre here), then trips turbulent.
wrote C:\Users\acwic\symplectic-nbody\examples\output\grashof.svgThe Womersley number: why blood flow lags the pulse
A pulsating pressure does not make a pulsating parabola. The Womersley number alpha = R sqrt(omega/nu) compares the heartbeat frequency to how fast viscosity diffuses momentum across a vessel. Small alpha (arterioles, capillaries) stays quasi-steady -- an in-phase Poiseuille parabola; large alpha (the aorta at alpha ~ 15) has too much core inertia to follow, so the flow lags the pressure by up to 90 degrees and flattens into a blunt plug with the shear squeezed into a thin wall layer. The pressure pulse itself races ahead at the Moens-Korteweg speed.
Womersley number alpha = R sqrt(omega/nu): does flow keep up with the heartbeat? Blood nu = 3.5e-6 m^2/s, heart rate 60 bpm vessel radius alpha phase lag regime aorta 11.00mm 14.74 86 deg plug (inertial) large artery 4.00mm 5.36 69 deg transitional arteriole 0.15mm 0.20 0 deg quasi-steady capillary 0.00mm 0.01 0 deg quasi-steady Aortic pulse-wave speed (Moens-Korteweg): 6.5 m/s -- the pressure pulse races down the arterial tree far faster than the blood itself moves. High alpha in the aorta flattens the profile to a plug and makes the flow lag the pressure by ~90 deg. wrote C:\Users\acwic\symplectic-nbody\examples\output\womersley.svg
The Marangoni effect: flow along a tension gradient
When surface tension varies along a surface -- from a temperature or composition gradient -- the imbalance drags the fluid from low-tension toward high-tension regions. It climbs the tears of wine up a glass, scatters pepper from a soap drop, and stirs weld pools. The Marangoni number Ma = |dgamma/dT| dT L/(mu alpha) measures the drive against diffusion, breaking a heated layer into Benard-Marangoni cells above Ma ~ 80. The dynamic Bond number Ra/Ma decides surface tension vs buoyancy: thin films and microgravity are Marangoni-driven, thick pools on the ground buoyancy-driven.
Marangoni effect: flow from a surface-tension gradient (Ma_c ~ 80)
Heated water layer, dT = 10 K (dgamma/dT = -1.5e-4 N/m/K):
layer L Ma onset? flow U
0.05 mm 5.4e+02 convects 1.50 m/s
0.20 mm 2.1e+03 convects 1.50 m/s
1.00 mm 1.1e+04 convects 1.50 m/s
5.00 mm 5.4e+04 convects 1.50 m/s
Tears of wine: ethanol evaporates from the film climbing the glass, raising its
surface tension there, so liquid is pulled UP toward the higher-tension rim until
it beads and runs back as 'legs'. A pure solutal Marangoni flow, no heat needed.
Marangoni vs buoyancy (dynamic Bond number Bo_d = Ra/Ma):
thin film, Earth Bo_d = 1.37e-04 -> Marangoni-driven
thick pool, Earth Bo_d = 1.37e+00 -> buoyancy-driven
thick pool, orbit Bo_d = 1.40e-05 -> Marangoni-driven
wrote C:\Users\acwic\symplectic-nbody\examples\output\marangoni.svgKutta-Joukowski: lift is circulation
A wing flies because the flow around it carries a net swirl -- circulation -- and the Kutta-Joukowski theorem makes it exact: lift per span L' = rho U Gamma. The airfoil sets its own circulation through the Kutta condition (smooth flow off the trailing edge), giving the thin-airfoil lift-slope c_l = 2 pi alpha. The same theorem is the Magnus effect -- a spinning ball drags a boundary layer around, generating circulation and a sideways curve. Lift isn't free: finite wings trail vortices and pay induced drag c_l^2/(pi AR e), so soaring birds wear long thin wings.
Kutta-Joukowski: L' = rho U Gamma, and thin-airfoil c_l = 2 pi alpha
angle of attack c_l (thin)
0 deg 0.000
2 deg 0.219
5 deg 0.548
8 deg 0.877
12 deg 1.316
Light aircraft (20 m^2 wing, c_l = 0.5, 50 m/s):
lift = 15.3 kN (holds ~1.6 tonnes)
Magnus effect on a spinning ball (side force):
ball spin speed side force
tennis topspin 3000 rpm 25 m/s 4.34 N
football curl 600 rpm 25 m/s 32.18 N
golf backspin 3000 rpm 60 m/s 2.75 N
Lift comes from the circulation the wing sets via the Kutta condition, not any
equal-transit myth. Finite wings trail vortices and pay induced drag c_l^2/(pi AR e),
which is why soaring birds and gliders wear long, high-aspect-ratio wings.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kutta_joukowski.svgThe Knudsen number: when a gas stops being a fluid
A gas behaves as a smooth continuum only while its mean free path lambda is tiny next to the system size L. Their ratio, the Knudsen number Kn = lambda/L, sorts flows into continuum (Kn<0.01, ordinary Navier-Stokes), slip, transitional and free-molecular (Kn>10, molecules fly wall-to-wall) regimes. Air's sea-level lambda ~ 68 nm makes everything macroscopic a perfect fluid -- but shrink L to a MEMS channel or a nanopore, or thin the air at orbital altitude, and Kn climbs past 1, so the gas slips at walls and finally must be computed molecule by molecule.
Knudsen number Kn = lambda / L: continuum fluid vs free-flying molecules Air mean free path: 67.2 nm at sea level, mean speed 468 m/s (300 K) system L P Kn regime airliner wing 3.0e+00 1.0e+05 2.24e-08 continuum insect wing 1.0e-03 1.0e+05 6.72e-05 continuum MEMS microchannel 1.0e-06 1.0e+05 6.72e-02 slip nanopore filter 5.0e-09 1.0e+05 1.34e+01 free molecular satellite (100 km) 1.0e+00 3.0e-02 2.27e-01 transitional vacuum chamber 1.0e-01 1.0e-03 6.81e+01 free molecular Everyday air is a flawless fluid because lambda ~ 68 nm is dwarfed by anything we touch. Shrink to a chip's cooling pores, or thin the air at orbital altitude, and Kn climbs past 1: the gas slips at walls and finally flies molecule-to-molecule. wrote C:\Users\acwic\symplectic-nbody\examples\output\knudsen.svg
The Richardson number: shear vs stratification
A stably stratified fluid resists overturning, but fast enough shear can rip the interface into billows anyway. The gradient Richardson number Ri = N^2/(du/dz)^2 weighs the buoyant stiffness against the squared shear: the Miles-Howard theorem guarantees stability where Ri > 1/4, and below it the Kelvin-Helmholtz instability curls the interface into cat's-eye billows. It governs clear-air turbulence that jolts aircraft, mixing in the ocean thermocline (Ri ~ 4, layered) and the entrainment atop a fog layer.
Richardson number Ri = N^2 / (du/dz)^2: buoyant stiffness vs shear (KH at Ri<1/4) layer N (rad/s) shear Ri state ocean thermocline 0.010 0.005 4.00layered (stable) nocturnal inversion 0.020 0.020 1.00layered (stable) jet-stream shear zone 0.012 0.030 0.16 KH billows breaking billows 0.008 0.050 0.03 KH billows For N = 0.012 rad/s the critical shear is 0.0240 /s: steeper than that drops Ri below 1/4 and the interface rolls up into Kelvin-Helmholtz cat's-eye billows -- the same physics behind clear-air turbulence that jolts aircraft and cloud-edge waves. wrote C:\Users\acwic\symplectic-nbody\examples\output\richardson.svg
The Kolmogorov cascade: turbulence shredding into heat
Turbulence hands energy down a cascade: big eddies break into smaller ones until viscosity smears the tiniest into heat. In the inertial range between, statistics depend only on the dissipation rate epsilon, giving Kolmogorov's E(k) ~ epsilon^(2/3) k^(-5/3) -- the -5/3 law seen from wind tunnels to interstellar gas. The cascade ends at the Kolmogorov scale eta = (nu^3/epsilon)^(1/4), where the eddy Reynolds number is 1, and the span L/eta ~ Re^(3/4) makes turbulence cost ~Re^(9/4) grid points to simulate in 3-D.
Kolmogorov cascade: big eddies -> small eddies -> heat, E(k) ~ k^(-5/3) flow Re eta tau_eta L/eta stirred coffee 5.0e+03 0.084mm 7.07e-03s 5.9e+02 room air current 1.0e+05 0.533mm 1.90e-02s 5.6e+03 river reach 1.0e+07 0.056mm 3.16e-03s 1.8e+05 atmosphere (km) 6.7e+08 0.241mm 3.87e-03s 4.1e+06 Energy is fed in at the large scale, cascades untouched through the inertial range as E(k) ~ k^(-5/3), and is dissipated only at the Kolmogorov scale eta where the eddy Reynolds number drops to 1. The range widens as Re^(3/4), so a weather-scale flow spans millions of eddy sizes -- why turbulence is so costly to simulate (~Re^(9/4) grid points in 3-D). wrote C:\Users\acwic\symplectic-nbody\examples\output\kolmogorov.svg
The Casimir effect: pushed together by empty space
The quantum vacuum has a zero-point energy in every field mode. Slide two conducting plates close and only the modes that fit in the gap survive between them, so the fuller vacuum outside presses them together with a pressure P = pi^2 hbar c/(240 d^4) -- a force from nothing but the structure of empty space, predicted in 1948 and measured in 1997. The steep d^-4 law makes it invisible at human gaps but crushing below 100 nm (an atmosphere by ~10 nm), where it sticks micro-machine parts together.
Casimir effect: vacuum pushes two plates together, P = pi^2 hbar c / (240 d^4)
gap d pressure force on 1 cm^2
10 nm 1.30e+05 Pa 13001257.724 uN
100 nm 1.30e+01 Pa 1300.126 uN
1 um 1.30e-03 Pa 0.130 uN
10 um 1.30e-07 Pa 1.30e-11 N
1 mm 1.30e-15 Pa 1.30e-19 N
At d = 10.6 nm the Casimir pressure equals one atmosphere. The force is pure
quantum vacuum -- only the modes that fit between the plates survive inside, so
the fuller outside vacuum presses them together. The d^-4 law makes it invisible
at human scales but dominant in MEMS, where it sticks micro-parts together (stiction).
wrote C:\Users\acwic\symplectic-nbody\examples\output\casimir.svgThe Hall effect: weighing carriers with a magnet
Run a current through a conductor in a perpendicular field and the Lorentz force pushes the carriers sideways until a transverse Hall voltage V_H = I B/(n q t) balances them. Its magnitude gives the carrier density n, and its sign reveals whether the charge carriers are electrons or positive holes -- the result that classical free-electron theory could not explain and that underpins semiconductor doping. Combined with the conductivity it separates density from mobility (mu = |R_H| sigma), so a Hall bar fully characterizes a conductor.
Hall effect: V_H = I B / (n q t) reveals carrier density AND sign 1 mA through a 0.1 mm-thick sample in a 1 T field: material n (1/m^3) carrier V_H |R_H| copper (electrons) 8.5e+28 electrons 0.001 uV 7.34e-11 aluminium (holes*) 1.8e+29 holes 0.000 uV 3.47e-11 n-Si (doped) 1.0e+22 electrons 6.24 mV 6.24e-04 p-Ge (doped) 5.0e+21 holes 12.48 mV 1.25e-03 (*Al's measured Hall sign is positive -- a famous hint that band structure makes some carriers behave as positive 'holes', which classical free electrons can't explain.) The doped semiconductors give millivolt Hall signals from their sparse carriers, vs microvolts in a metal: fewer carriers, bigger V_H. Mobility follows from mu = |R_H| sigma -- n-Si here: mu = 0.624 m^2/Vs, Hall angle 32.0 deg at 1 T. wrote C:\Users\acwic\symplectic-nbody\examples\output\hall_effect.svg
Wiedemann-Franz: good conductors of charge and heat
In a metal the same free electrons carry charge and heat, so their conductivities are locked together: kappa/(sigma T) = L, the Lorenz number pi^2 k_B^2/(3 e^2) = 2.44e-8 W ohm/K^2. The material-specific mean free path and carrier density cancel in the ratio, leaving only fundamental constants -- so you can read a metal's thermal conductivity off an easy resistance measurement (copper's ~400 W/(m K) from its sigma). A measured Lorenz number well below L flags heat and charge decoupling, the signature of exotic 'strange metals'.
Wiedemann-Franz: kappa / (sigma T) = L = 2.443e-08 W ohm / K^2 (T = 300 K) metal sigma (S/m) kappa pred kappa meas L_eff/L silver 6.30e+07 462 429 0.93 copper 5.96e+07 437 401 0.92 gold 4.10e+07 300 318 1.06 aluminium 3.77e+07 276 237 0.86 iron 1.00e+07 73 80 1.09 lead 4.55e+06 33 35 1.05 The same free electrons carry both currents, so the material-specific mean free path and carrier density cancel: kappa/(sigma T) is a near-universal constant. That lets you read a metal's heat conductivity off an easy resistance measurement -- and a measured L well below 2.44e-8 flags heat and charge decoupling (strange metals). wrote C:\Users\acwic\symplectic-nbody\examples\output\wiedemann_franz.svg
Bragg diffraction: reading a crystal with X-rays
Shine X-rays on a crystal and bright reflections flash only where waves scattered from successive atomic planes add in phase: n lambda = 2 d sin(theta). Because the wavelength must match the ~0.1-0.5 nm atomic spacing, X-rays are the natural probe, and reading the diffraction spots backwards gives the structure -- the method that solved salt, DNA and countless proteins. For a cubic lattice each Miller plane (hkl) has its own spacing a/sqrt(h^2+k^2+l^2) and family of Bragg angles, and sin(theta) <= 1 caps the visible orders at 2d/lambda.
Bragg's law n lambda = 2 d sin(theta): X-ray crystallography
Cu K-alpha (0.1541 nm) on silicon (a = 0.543 nm)
(hkl) d (nm) 1st-order angle max order
(1, 1, 1) 0.3135 14.23 deg 4
(2, 2, 0) 0.1920 23.66 deg 2
(3, 1, 1) 0.1637 28.07 deg 2
(4, 0, 0) 0.1358 34.58 deg 1
Orders off the (111) planes (d = 0.3135 nm):
n = 1 -> theta = 14.23 deg
n = 2 -> theta = 29.44 deg
n = 3 -> theta = 47.50 deg
n = 4 -> theta = 79.45 deg
Each family of planes flashes a reflection only where its path difference
2 d sin(theta) is a whole number of wavelengths. Reading the spots backwards gives
the atomic spacings -- how crystallography solved salt, DNA, and countless proteins.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bragg.svgThe diffraction limit: every aperture's resolution floor
No lens focuses light to a point: an aperture of diameter D spreads a wave into an Airy disk of angular radius theta = 1.22 lambda/D, the Rayleigh criterion, so two sources closer than that blur into one. Bigger apertures resolve finer detail (Hubble's 2.4 m gives ~0.05 arcsec), long wavelengths need huge ones (radio dishes), and a microscope stops at the Abbe limit lambda/(2 NA) ~ 200 nm for light -- which is why electron microscopes with picometre wavelengths see atoms. A grating turns it into a spectrometer of resolving power m N.
Diffraction limit: theta = 1.22 lambda / D -- aperture sets resolution
instrument aperture lambda resolution
human eye 2mm 550 nm 1.2 arcmin
binoculars (50 mm) 50mm 550 nm 2.768 arcsec
amateur scope (200 mm) 200mm 550 nm 0.692 arcsec
Hubble (2.4 m) 2 m 550 nm 0.058 arcsec
VLT (8.2 m) 8 m 550 nm 0.017 arcsec
Arecibo radio (305 m) 305 m 21 cm 2.9 arcmin
Microscope (Abbe d = lambda/2NA):
light, NA 1.4: 196 nm
electron, 4 pm: 100.0 pm (resolves atoms)
A 10000-line grating resolves lambda/dlambda = 10000 -- enough to split the
sodium doublet (589.0 vs 589.6 nm). Bigger apertures and more grating lines are
the only way past the wave-optics floor; it's why telescopes and dishes grow huge.
wrote C:\Users\acwic\symplectic-nbody\examples\output\diffraction_limit.svgSnell's law: bending, trapping, and reflecting light
Light changes speed across a boundary and so must bend: n1 sin(theta1) = n2 sin(theta2). Entering a denser medium it turns toward the normal; leaving one, away from it -- until, past the critical angle arcsin(n2/n1), the refracted ray cannot exist and all the light is totally internally reflected. That perfect mirror guides light down an optical fibre and makes a diamond (24 deg critical angle) sparkle. At Brewster's angle arctan(n2/n1) the reflection is perfectly polarized, the trick behind polarizing sunglasses.
Snell's law n1 sin(t1) = n2 sin(t2): refraction, TIR, and Brewster
medium n critical angle Brewster (from air)
water 1.333 48.6 deg 53.1 deg
glass 1.520 41.2 deg 56.7 deg
diamond 2.417 24.4 deg 67.5 deg
Diamond's tiny 24 deg critical angle traps light through many internal bounces
before it escapes -- that repeated total internal reflection is the sparkle.
Air -> water refraction (light bends toward the normal entering denser water):
incidence 10 deg -> refracts to 7.5 deg
incidence 30 deg -> refracts to 22.0 deg
incidence 50 deg -> refracts to 35.1 deg
incidence 70 deg -> refracts to 44.8 deg
Optical fibre (core 1.48, clad 1.46): NA = 0.242, acceptance cone half-angle 14.0 deg.
Light inside that cone is trapped by total internal reflection and guided for km.
wrote C:\Users\acwic\symplectic-nbody\examples\output\snell.svgThin-film interference: bubble colours and lens coatings
A transparent film reflects light off both surfaces, and the two waves interfere by the round-trip path 2 n t -- so a film only nanometres thick paints itself in colour, the sheen of a soap bubble or an oil slick. A half-wave phase flip on the denser-medium reflection sets which colours brighten or cancel, and is why a soap film goes black just before it bursts (2 n t -> 0, destructive everywhere). Engineered as a quarter-wave layer t = lambda/(4n) of index sqrt(n_substrate), the same interference cancels reflection -- the anti-glare coating on every lens. Newton's rings are its fringes in an air gap.
Thin-film interference: a film nm-thick paints itself in colour
Soap film (n = 1.33) -- first-order bright reflected wavelength:
thickness 2 n t bright lambda colour
80 nm 213 nm 426 nm violet
110 nm 293 nm 585 nm yellow
150 nm 399 nm 798 nm infrared
200 nm 532 nm 1064 nm infrared
250 nm 665 nm 1330 nm infrared
Anti-reflection coating for a glass lens (n_glass = 1.52) at 550 nm:
ideal index = sqrt(1.52) = 1.233 (MgF2 at 1.38 is the practical pick)
quarter-wave MgF2 thickness = 100 nm
-> the two reflections cancel, killing glare and boosting transmission.
A soap film thins to near-zero before it bursts: 2 n t -> 0 is destructive at
every colour, so the film looks BLACK -- the classic sign it is about to pop.
wrote C:\Users\acwic\symplectic-nbody\examples\output\thin_film.svgMalus's law: dialling light down with polarizers
A polarizer passes only the field component along its axis, so linearly polarized light emerges at I = I0 cos^2(theta) -- Malus's law. Unpolarized light loses exactly half through any one polarizer, and two crossed at 90 degrees pass nothing (the dark LCD pixel); yet slipping a third at 45 degrees between them rescues I0/8, light where there was none. A stack of many slightly rotated polarizers drags the polarization around while passing nearly all the light -- an optical quantum Zeno effect -- and wave plates rotate it losslessly by retarding one component.
Malus's law I = I0 cos^2(theta): a polarizer passes the aligned field component
angle transmission
0 deg 1.000
30 deg 0.750
45 deg 0.500
60 deg 0.250
90 deg 0.000
Crossed polarizers (90 deg apart) pass nothing: 0.000
...but a third polarizer at 45 deg between them rescues 0.125 of the input
(I0/8) -- light reappears where two polarizers alone gave darkness.
Rotating a stack of N polarizers through 90 deg (quantum-Zeno-like):
N = 1 -> throughput 0.000
N = 2 -> throughput 0.250
N = 5 -> throughput 0.605
N = 20 -> throughput 0.884
N = 100 -> throughput 0.976
With one polarizer a 90 deg turn kills the light; with many tiny steps it
survives -- each cos^2 is nearly 1, so the polarization is dragged around intact.
wrote C:\Users\acwic\symplectic-nbody\examples\output\malus.svgCherenkov radiation: the blue glow of going too fast
Light slows to c/n in a medium, and a charged particle can outrun it. When beta > 1/n it drags an electromagnetic shock cone behind it -- an optical sonic boom -- radiating the blue glow of a reactor pool. The cone half-angle obeys cos(theta) = 1/(n beta), just like a Mach cone, opening from threshold toward a maximum arccos(1/n) (~41 deg in water) as the particle nears beta = 1. Because the angle reads off the velocity, ring-imaging Cherenkov detectors use it to identify particles, and neutrino observatories watch for the faint cones.
Cherenkov radiation: cos(theta) = 1/(n beta), emit only if beta > 1/n
medium beta_thr gamma_thr max cone
water (n=1.33) 0.750 1.51 41.4 deg
glass (n=1.52) 0.658 1.33 48.9 deg
aerogel (n=1.05) 0.952 3.28 17.8 deg
Cone angle vs speed in water:
beta = 0.76 -> theta = 9.2 deg, rel. photon yield 0.026
beta = 0.85 -> theta = 28.0 deg, rel. photon yield 0.221
beta = 0.95 -> theta = 37.8 deg, rel. photon yield 0.376
beta = 0.999 -> theta = 41.3 deg, rel. photon yield 0.436
A charged particle outrunning light-in-medium sheds an EM shock cone -- the
optical sonic boom that glows blue in a reactor pool. The cone angle reads off
the velocity, so ring-imaging Cherenkov detectors use it to identify particles.
wrote C:\Users\acwic\symplectic-nbody\examples\output\cherenkov.svgThe Zeeman effect: splitting lines with a magnetic field
An atom's magnetic moment shifts its energy levels in a field, so a spectral line splits: delta_E = g_J m_J mu_B B. The normal Zeeman effect (spin cancels, g=1) gives a clean Lorentz triplet shifted by mu_B B/h = 14 GHz per tesla; the anomalous effect (g != 1, from the Lande factor 1 + [J(J+1)+S(S+1)-L(L+1)]/2J(J+1)) splits into more, unevenly spaced lines whose existence forced the discovery of electron spin. Reading the splitting backwards measures the field -- how magnetograms map sunspots.
Zeeman effect: delta_E = g_J m_J mu_B B splits levels (mu_B = 9.274e-24 J/T)
field normal split at 500 nm
0.1 T 1.40 GHz 1.17 pm
0.3 T 4.20 GHz 3.50 pm
1.0 T 14.00 GHz 11.67 pm
3.0 T 41.99 GHz 35.01 pm
Lande g-factors (anomalous Zeeman -- uneven splitting):
2S1/2 (ground) g = 2.000
2P1/2 g = 0.667
2P3/2 g = 1.333
3D3 (pure orbital-ish) g = 1.333
A sunspot line split by 4.2 GHz implies B = 0.30 T -- how magnetograms map the
Sun's magnetic field. The normal triplet (g=1) is the Lorentz-triplet classical
physics got right; the uneven anomalous patterns forced the discovery of spin.
wrote C:\Users\acwic\symplectic-nbody\examples\output\zeeman.svgRabi oscillations: a two-level atom flopping
A near-resonant field does not just excite a two-level system once -- it cycles it coherently between ground and excited at the Rabi frequency Omega = dE/hbar. On resonance P_e(t) = sin^2(Omega t/2) swings fully 0 to 1, so a pi pulse inverts the population (a qubit X gate) and a pi/2 pulse builds an equal superposition. Detuned by delta the flopping runs faster, at sqrt(Omega^2 + delta^2), but only reaches Omega^2/(Omega^2 + delta^2) -- a Lorentzian resonance of width Omega. These are the elementary operations of atomic clocks and quantum bits.
Rabi oscillations: P_e(t) = (O^2/O_R^2) sin^2(O_R t/2), O = 2pi x 1 MHz
Pulses on resonance:
pi pulse (full inversion, X gate): 500 ns
pi/2 pulse (equal superposition): 250 ns
Detuning kills contrast (peak excitation P_max = O^2/(O^2+d^2)):
detuning gen. Rabi peak P_e
0.0 MHz 1.00 MHz 1.000
0.5 MHz 1.12 MHz 0.800
1.0 MHz 1.41 MHz 0.500
2.0 MHz 2.24 MHz 0.200
On resonance the atom swings all the way to the excited state and back; detuned,
it oscillates faster but only part-way. A pi pulse flips a qubit, a pi/2 pulse
builds a superposition -- the elementary gates of atomic clocks and quantum bits.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rabi.svgThe Franck-Hertz experiment: energy levels in a current
Fire electrons through mercury vapour and ramp the accelerating voltage: the collected current climbs, then drops sharply every 4.9 V. Electrons collide elastically until they gain the atom's excitation energy, then dump exactly that quantum in an inelastic collision and arrive too slow to be collected -- so the current falls, and the evenly spaced dips are direct proof that atomic energy is quantized (the 1914 confirmation of the Bohr atom). The excited atom relaxes by emitting a photon at that energy, mercury's 254 nm UV line.
Franck-Hertz: current dips prove quantized atomic energy (mercury, 4.9 eV)
Current dips at multiples of the excitation voltage:
dip 1: 4.9 V (electron has excited the atom 1 time)
dip 2: 9.8 V (electron has excited the atom 2 times)
dip 3: 14.7 V (electron has excited the atom 3 times)
dip 4: 19.6 V (electron has excited the atom 4 times)
dip 5: 24.5 V (electron has excited the atom 5 times)
Dip spacing = 4.9 V = the mercury 6s6p excitation energy in volts.
Emission on relaxation: lambda = h c / E = 253 nm (the mercury UV line).
accel V excitations residual (eV)
3 V 0 3.00
6 V 1 1.10
11 V 2 1.20
16 V 3 1.30
25 V 5 0.50
Electrons collide elastically (no energy lost) until they reach 4.9 eV, then can
dump exactly that quantum inelastically and arrive too slow to be collected -- so
the current drops. Repeating dips = repeated excitations = energy is quantized.
wrote C:\Users\acwic\symplectic-nbody\examples\output\franck_hertz.svgMoseley's law: ordering the elements by X-ray colour
An element struck by fast electrons fluoresces characteristic X-rays, and Moseley found the square root of the K-alpha frequency rises linearly with atomic number, sqrt(f) = a(Z-1). Equivalently the K-alpha energy is a Rydberg-like 13.6 (3/4)(Z-1)^2 eV -- copper's 8 keV, molybdenum's 17 keV. This ordered the periodic table by nuclear charge rather than atomic weight, exposed gaps where undiscovered elements had to sit, and is still how an XRF gun reads which elements a sample contains from its X-ray lines.
Moseley's law: sqrt(f) = a(Z - 1) -- X-ray lines order the periodic table
element Z K-alpha energy wavelength
Ca 20 3.68 keV 0.3366 nm
Fe 26 6.38 keV 0.1944 nm
Cu 29 8.00 keV 0.1550 nm
Mo 42 17.15 keV 0.0723 nm
Ag 47 21.59 keV 0.0574 nm
W 74 54.38 keV 0.0228 nm
Identifying an unknown from its K-alpha line:
line at 6.4 keV -> Z = 26 (Fe)
line at 8.0 keV -> Z = 29 (Cu)
line at 21.6 keV -> Z = 47 (Ag)
Because sqrt(f) is exactly linear in Z, Moseley's plot ordered the elements by
nuclear charge -- not atomic weight -- and its gaps predicted where undiscovered
elements (technetium, promethium) had to be. Still the basis of XRF elemental ID.
wrote C:\Users\acwic\symplectic-nbody\examples\output\moseley.svgThe Stark effect: electric fields on atoms
The electric analogue of Zeeman: a field shifts atomic levels and splits lines. Hydrogen's degenerate levels give a LINEAR Stark effect -- a shift proportional to the field, since they mix into a permanent dipole -- fanning level n into 2n-1 equally spaced components. Most atoms have no permanent dipole and shift quadratically, -1/2 alpha E^2, always lowering the energy. Push hard enough and the field strips the electron: the ionization threshold scales as 1/n^4, so a Rydberg atom ionizes in a field ten billion times weaker than the ground state.
Stark effect: an electric field splits (hydrogen: linearly) and ionizes atoms
Linear Stark components of hydrogen at 5 MV/m (energy shift, ueV):
n=2: 3 lines [-793.8, +0.0, +793.8]
n=3: 5 lines [-2381.3, -1190.6, +0.0, +1190.6, +2381.3]
n=4: 7 lines [-4762.6, -3175.1, -1587.5, +0.0, +1587.5, +3175.1, +4762.6]
Field-ionization threshold vs principal quantum number:
n binding (eV) ionizing field
1 13.6057 3.21e+10 V/m
5 0.5442 5.14e+07 V/m
10 0.1361 3.21e+06 V/m
30 0.0151 3.97e+04 V/m
50 0.0054 5.14e+03 V/m
Hydrogen's degenerate levels give the LINEAR Stark effect (a permanent dipole);
most atoms shift quadratically as -1/2 alpha E^2. And because binding ~ 1/n^2, a
Rydberg atom ionizes in a field ten billion times weaker than the ground state --
which is how Rydberg-atom detectors sense tiny fields and single microwave photons.
wrote C:\Users\acwic\symplectic-nbody\examples\output\stark.svgThe Aharonov-Bohm effect: a phase from an untouched field
Classically no field means no effect, but a charged particle steered around a solenoid -- with the field entirely confined inside, zero on its path -- still has its interference fringes shift. It responds to the vector potential, picking up a phase delta_phi = q Phi/hbar set purely by the enclosed flux, proof that the potentials are physically real in quantum mechanics. The phase is periodic in the flux quantum h/q (h/2e for Cooper pairs), which quantizes flux through a superconducting ring and drives SQUID magnetometers to sense fields a billion times weaker than Earth's.
Aharonov-Bohm: delta_phi = q Phi / hbar, from flux Phi with B=0 on the path
Electron flux quantum Phi_0 = h/e = 4.136e-15 Wb
flux / Phi_0 phase (rad) fringe shift
0.00 0.000 0.00
0.25 1.571 0.25
0.50 3.142 0.50
1.00 6.283 0.00
2.50 15.708 0.50
Superconducting flux quantum h/2e = 2.068e-15 Wb (Cooper pairs).
One quantum through a 1 mm^2 SQUID loop needs only 2.07e-09 T -- how SQUIDs sense
fields a billion times weaker than Earth's. The electron never sees the field,
only the vector potential -- proof the potentials are physically real in QM.
wrote C:\Users\acwic\symplectic-nbody\examples\output\aharonov_bohm.svgThe Josephson junction: a supercurrent that defines the volt
Cooper pairs tunnel through a thin barrier between superconductors with zero voltage, a supercurrent I = I_c sin(phi) set only by the quantum phase difference (DC effect). Apply a DC voltage and the phase winds, so the current oscillates at the Josephson frequency f = 2eV/h = 483.6 GHz per millivolt -- an exact voltage-to-frequency conversion through only e and h. Irradiating the junction locks it onto quantized Shapiro voltage steps n h f/2e, which is how the SI volt is now defined and how the most accurate voltmeters work.
Josephson junction: I = I_c sin(phi), f = 2eV/h = 483.6 GHz/mV
Josephson constant K_J = 2e/h = 4.8360e+14 Hz/V
voltage Josephson freq
10 uV 4.84 GHz
100 uV 48.36 GHz
1000 uV 483.60 GHz
Shapiro steps under 70 GHz irradiation (the volt standard):
step 1: 144.75 uV
step 2: 289.50 uV
step 5: 723.74 uV
step 10: 1447.48 uV
A 1 uA junction has coupling energy E_J = 3.29e-22 J (496.7 GHz x h).
Cooper pairs tunnel the barrier with zero voltage (DC effect); a DC voltage makes
the phase wind and the current oscillate (AC effect). The exact V<->f link, tied
only to e and h, is how the volt is now defined and how quantum voltmeters work.
wrote C:\Users\acwic\symplectic-nbody\examples\output\josephson.svgThe quantum Hall effect: resistance from pure constants
Cool a 2D electron gas in a strong field and its Hall resistance locks onto flat plateaus R_xy = R_K/nu with R_K = h/e^2 = 25812.807 ohm -- values set by fundamental constants alone, independent of the material. The electron energies collapse into Landau levels (spacing hbar eB/m, degeneracy eB/h), and when nu of them are filled the bulk is insulating while nu chiral edge channels each carry e^2/h of conductance. Reproducible to parts per billion in any device, it now defines the ohm -- the resistance counterpart of the Josephson volt.
Quantum Hall effect: R_xy = R_K / nu, R_K = h/e^2 = 25812.807 ohm
filling nu Hall resistance
1 25812.8 ohm
2 12906.4 ohm
3 8604.3 ohm
4 6453.2 ohm
6 4302.1 ohm
Landau levels vs field (need k_B T << spacing to resolve plateaus):
B (T) spacing (meV) degeneracy (1/m^2)
2 0.23 4.84e+14
5 0.58 1.21e+15
10 1.16 2.42e+15
20 2.32 4.84e+15
A 2D electron gas at n = 5e15 /m^2 hits integer filling as field is swept:
B = 5 T -> nu = 4.14
B = 10 T -> nu = 2.07
B = 20 T -> nu = 1.03
On each plateau the bulk is insulating and only chiral edge channels conduct,
one per filled Landau level -- so R_xy depends on nothing but h/e^2. Reproducible
to parts per billion in any device, it is how the ohm is now defined.
wrote C:\Users\acwic\symplectic-nbody\examples\output\quantum_hall.svgBCS superconductivity: the gap that kills resistance
Below T_c a phonon-mediated attraction binds electrons into Cooper pairs that condense into one coherent state carrying current without resistance. The theory's core is an energy gap Delta at the Fermi surface -- it costs 2 Delta to break a pair, so nothing scatters the condensate. BCS predicts the universal ratio 2 Delta(0)/(k_B T_c) = 3.53 for every weak-coupling superconductor, a gap that closes as sqrt(1-T/Tc), and T_c = 1.13 hbar wD exp(-1/lambda) -- whose wD ~ 1/sqrt(M) gives the isotope effect that proved phonons do the pairing.
BCS superconductivity: 2 Delta(0) / (k_B T_c) = 3.53 (universal)
material T_c (K) gap (meV) pair-break
aluminium 1.2 0.183 0.09 THz
niobium 9.3 1.414 0.68 THz
lead 7.2 1.095 0.53 THz
Isotope effect (T_c ~ M^-1/2): mercury-198 vs mercury-202 (Hg T_c=4.15 K):
Hg-202 / Hg-198 mass ratio 1.020 -> T_c = 4.109 K
heavier isotope, lower T_c -- proof phonons do the pairing.
T_c from electron-phonon coupling (Debye freq 3e13 rad/s):
lambda = 0.2 -> T_c = 1.74 K
lambda = 0.3 -> T_c = 9.24 K
lambda = 0.4 -> T_c = 21.25 K
lambda = 0.5 -> T_c = 35.04 K
Below T_c an energy gap opens at the Fermi surface: it costs 2 Delta to break a
Cooper pair, so nothing scatters the condensate and resistance vanishes. The gap
closes as sqrt(1 - T/Tc) toward T_c, where superconductivity ends.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bcs.svgLondon & Meissner: expelling the magnetic field
A superconductor doesn't just conduct perfectly -- it actively pushes magnetic field out (the Meissner effect), which is why magnets levitate above one. The London equations give the field decaying into the surface as exp(-x/lambda_L) over the penetration depth lambda_L = sqrt(m/(mu0 n_s q^2)), tens of nanometres. The ratio kappa = lambda_L/xi to the coherence length splits superconductors into type I (kappa < 1/sqrt2, full expulsion) and type II (kappa > 1/sqrt2, quantized flux vortices) -- the latter surviving the huge fields of MRI and fusion magnets.
London/Meissner: B(x) = B0 exp(-x/lambda_L), lambda_L = sqrt(m/(mu0 n_s q^2))
Penetration depth vs superconducting carrier density:
n_s = 1e+28 /m^3 -> lambda_L = 37.6 nm
n_s = 4e+28 /m^3 -> lambda_L = 18.8 nm
n_s = 1e+29 /m^3 -> lambda_L = 11.9 nm
Type classification (kappa = lambda_L/xi, boundary 1/sqrt2 = 0.707):
material lambda (nm) xi (nm) kappa type
aluminium 16 1600 0.01 I
tin 34 230 0.15 I
niobium 40 38 1.05 II
Nb-Ti 300 4 75.00 II
YBCO (high-Tc) 150 1.5 100.00 II
Type I expels field until it abruptly goes normal; type II lets field thread
through as quantized vortices (each carrying 2.07e-15 Wb) between H_c1 and H_c2 --
which is how Nb-Ti and high-Tc magnets survive the huge fields of MRI and fusion.
wrote C:\Users\acwic\symplectic-nbody\examples\output\london.svgMean-field ferromagnetism: order from disorder
Below a Curie temperature a magnet spontaneously aligns -- countless spins tip into one direction with no applied field. Weiss mean-field theory of the Ising model captures it: each spin feels the average of its neighbours, m = tanh((z J m + B)/T), with T_c = z J. Above T_c the only zero-field solution is m = 0 (paramagnet); below it a nonzero magnetization appears, vanishing near T_c as (1 - T/Tc)^(1/2) (the mean-field beta = 1/2), while the susceptibility diverges as the Curie-Weiss 1/(T - T_c) -- the hallmarks of a second-order phase transition.
Mean-field Ising: m = tanh((z J m + B)/T), T_c = z J = 6
T / T_c magnetization phase
0.20 1.000 ferromagnet
0.50 0.958 ferromagnet
0.80 0.710 ferromagnet
0.95 0.379 ferromagnet
1.00 0.010 paramagnet
1.20 0.000 paramagnet
Curie-Weiss susceptibility above T_c (chi = C/(T - T_c)):
T = 1.05 T_c -> chi = 3.333
T = 1.20 T_c -> chi = 0.833
T = 1.50 T_c -> chi = 0.333
T = 2.00 T_c -> chi = 0.167
Below T_c countless spins tip collectively into one direction with no applied
field -- spontaneous symmetry breaking. The magnetization vanishes as (1-T/Tc)^1/2
(mean-field beta=1/2) and the susceptibility diverges at T_c: a phase transition.
wrote C:\Users\acwic\symplectic-nbody\examples\output\ising_mft.svgPercolation: the sudden onset of connectivity
Occupy each lattice site with probability p and ask if a connected path spans the system. Below a sharp threshold p_c (~0.59 for a 2D square lattice) the occupied sites form isolated islands; above it a single cluster abruptly spans the whole lattice -- a geometric phase transition. The largest-cluster fraction jumps from near zero to order one through p_c. The same threshold governs forest fires spreading, oil seeping through rock, disease jumping a contact network, and current finding a path through a random resistor grid.
Percolation: occupy sites with probability p, look for a spanning cluster
2D square-lattice site threshold p_c ~ 0.5927
p spans? largest cluster
0.40 0 % 1 %
0.50 0 % 3 %
0.55 13 % 9 %
0.59 60 % 30 %
0.65 100 % 50 %
0.75 100 % 75 %
Below p_c occupied sites form isolated islands; right at ~0.59 a single cluster
abruptly spans the whole lattice -- a geometric phase transition. Same threshold
governs forest fires, oil in rock, disease on a contact network, and random resistor grids.
wrote C:\Users\acwic\symplectic-nbody\examples\output\percolation.svgPolya's random walk: home, or lost forever?
A random walker on an infinite lattice steps to a random neighbour forever -- does it ever return to the origin? Polya proved the answer depends only on dimension: in 1D and 2D the walk is recurrent, returning with probability 1 (and visiting every site infinitely often); in 3D and above it is transient, escaping to infinity with nonzero probability (a ~0.34 chance of ever returning in 3D). The knife-edge is exactly two dimensions, because the probability of being back at the origin decays as n^(-d/2) -- summable only for d >= 3. 'A drunk man finds his way home, but a drunk bird may get lost forever.'
Polya's random walk: does an infinite lattice walk return to the origin?
'A drunk man finds his way home; a drunk bird may get lost forever.'
dim return prob escape prob exp. visits class
1 1.0000 0.0000 inf recurrent
2 1.0000 0.0000 inf recurrent
3 0.3405 0.6595 1.516 transient
4 0.1932 0.8068 1.239 transient
5 0.1352 0.8648 1.156 transient
Simulated return fraction (finite walks, 3000 steps, 200 trials):
1D -> 97 % returned
2D -> 67 % returned
3D -> 34 % returned
In 1D and 2D the walk is certain to return (and visits every site infinitely
often); in 3D+ it can escape to infinity. The knife-edge is exactly two dimensions,
because P(at origin) decays as n^(-d/2) -- summable (transient) only for d >= 3.
wrote C:\Users\acwic\symplectic-nbody\examples\output\polya.svgLangevin paramagnetism: moments vs thermal chaos
A paramagnet's independent magnetic moments each favour aligning with an applied field (energy -mu.B) while temperature randomizes them. Averaging over the Boltzmann distribution gives the Langevin function m/mu = L(x) = coth(x) - 1/x with x = mu B/(k_B T). Weak field or high temperature is the linear regime L(x) ~ x/3, so the susceptibility follows Curie's law chi ~ 1/T -- the fingerprint of a paramagnet; strong field or low temperature saturates every moment at L = 1 and the magnetization can grow no further.
Langevin paramagnetism: m/mu = L(x) = coth(x) - 1/x, x = mu B / kT
Alignment fraction L(x) for a 5-Bohr-magneton moment:
B (T) T (K) x aligned
1 300 0.011 0.4 %
10 300 0.112 3.7 %
10 4 8.396 88.1 %
50 1 167.928 99.4 %
Curie law chi = C/T (C = n mu^2/3k):
T = 1 K -> chi (per moment) = 5.191e-23
T = 10 K -> chi (per moment) = 5.191e-24
T = 100 K -> chi (per moment) = 5.191e-25
T = 300 K -> chi (per moment) = 1.730e-25
Weak field or high temperature: L(x) ~ x/3, so susceptibility falls as 1/T --
Curie's law, the fingerprint of a paramagnet. Strong field or low temperature:
every moment aligns and L saturates at 1, the magnetization can grow no further.
wrote C:\Users\acwic\symplectic-nbody\examples\output\langevin_para.svgBuffon's needle: estimating pi by dropping sticks
Rule a floor with parallel lines a distance d apart and drop a needle of length L <= d at random: it crosses a line with probability 2 L/(pi d). So counting crossings estimates pi -- pi ~ 2 L N/(d C) for N drops and C crossings -- the first problem in geometric probability (Buffon, 1777). pi emerges from a purely mechanical experiment with no measurement of pi anywhere, from the geometry of random position and angle. Convergence is the slow Monte Carlo 1/sqrt(N): 1% needs ~10000 drops, 0.1% about a million.
Buffon's needle: P(cross) = 2L/(pi d), so pi ~ 2 L N / (d C)
Crossing probability for L = d: 0.6366 (= 2/pi)
drops N crossings pi estimate error
100 58 3.44828 0.30668
1000 645 3.10078 0.04082
10000 6450 3.10078 0.04082
100000 63921 3.12886 0.01273
1000000 636951 3.13996 0.00163
Convergence is slow (Monte Carlo, error ~ 1/sqrt(N)):
5.0 % accuracy -> ~399 drops
1.0 % accuracy -> ~10,000 drops
0.1 % accuracy -> ~1,000,000 drops
pi falls out of a purely mechanical experiment -- counting how often a tossed
stick lands across a floorboard. No measurement of pi enters anywhere; it emerges
from the geometry of random position and angle. The first problem in geometric probability.
wrote C:\Users\acwic\symplectic-nbody\examples\output\buffon.svgMetropolis Monte Carlo: sampling the Ising transition
Systems too large to sum are sampled: propose a change and accept it with probability min(1, exp(-dE/T)) -- always downhill, Boltzmann-weighted uphill -- and the chain visits states with exactly the thermal probability exp(-E/T). Run on the 2D Ising ferromagnet it reproduces the real phase transition that mean-field theory only approximates: spins order below the exact Onsager T_c ~ 2.269 J/k_B and disorder above it, with genuine critical fluctuations -- domains at every scale near T_c -- that mean field cannot capture.
Metropolis MCMC: accept a flip with prob min(1, exp(-dE/T)) -- samples exp(-E/T)
2D Ising ferromagnet, exact Onsager T_c = 2.269 J/k_B
T (J/kB) avg |m| phase
1.00 0.999 ordered
1.80 0.959 ordered
2.27 0.763 ordered
2.60 0.543 ordered
3.50 0.084 disordered
The simulation reproduces the real phase transition mean-field theory only
approximates: spins order below ~2.27 J/k_B and disorder above it, with the true
Onsager critical temperature and genuine critical fluctuations near T_c.
wrote C:\Users\acwic\symplectic-nbody\examples\output\metropolis.svgThe logistic map: period doubling into chaos
The one-line map x' = r x(1-x) is the textbook birth of chaos. As the growth rate r rises, a stable population splits into a 2-cycle at r=3, then 4, 8, 16, ... in a cascade that accumulates at r ~ 3.5699 -- the onset of aperiodic, initial-condition-sensitive chaos, interrupted by periodic windows (the famous period-3 near 3.83). The bifurcation spacings shrink by the universal Feigenbaum constant 4.669, the same for any smooth single-humped map, and a positive Lyapunov exponent marks the chaotic regime.
Logistic map x' = r x (1-x): the route to chaos (Feigenbaum delta = 4.6692)
r period Lyapunov regime
2.50 1 -0.693 period-1
3.20 2 -0.916 period-2
3.50 4 -0.873 period-4
3.55 8 -0.100 period-8
3.83 3 -0.369 period-3
3.90 64 0.492 chaos
4.00 1 0.693 chaos
Period doublings 2,4,8,... accumulate at r ~ 3.5699, then chaos -- broken by
periodic windows (the period-3 near 3.83 is the most famous). The bifurcation
spacings shrink by the universal Feigenbaum ratio 4.669, the same for any smooth
unimodal map -- one of the deepest facts in nonlinear dynamics.
wrote C:\Users\acwic\symplectic-nbody\examples\output\logistic_map.svgThe Henon map: a strange attractor in two lines
Henon's 1976 map, x' = 1 - a x^2 + y, y' = b x, is the canonical low-dimensional strange attractor. At a=1.4, b=0.3 the iterates never settle and never repeat, tracing a fractal of nested arcs that -- zoomed in -- resolve into a Cantor set of ever-finer strands. It is dissipative (areas shrink by |b| each step) yet chaotic (largest Lyapunov exponent ~0.42): a blob is squeezed in area while stretched and folded, collapsing onto a fractal of dimension ~1.26. Chaos with structure at every scale.
Henon map: x'=1-a x^2+y, y'=b x (a=1.4, b=0.3)
Area contraction per step |det J| = |b| = 0.30 (dissipative -> attractor)
Fixed points:
(+0.6314, +0.1894)
(-1.1314, -0.3394)
Largest Lyapunov exponent = 0.421 nat/iteration (positive -> chaos).
Fractal (correlation) dimension ~ 1.26 -- between a curve and a filled region.
The map stretches and folds the plane each step: a blob shrinks in area (by |b|)
yet is pulled apart along the unstable direction, so it collapses onto a fractal
of nested arcs. Zoom in and each arc is really a Cantor set of finer strands --
the hallmark of a strange attractor, chaos with structure at every scale.
wrote C:\Users\acwic\symplectic-nbody\examples\output\henon.svgThe Lorenz attractor: the butterfly effect
Lorenz's three equations for toy convection never repeat and cannot be forecast for long. At sigma=10, beta=8/3, rho=28 the trajectory winds around two spiral lobes, jumping between them unpredictably -- the butterfly-shaped strange attractor. The flow is dissipative (phase volume shrinks at -(sigma+1+beta) so everything collapses onto the zero-volume fractal) yet chaotic: two starts a millionth apart diverge to opposite wings, the largest Lyapunov exponent ~0.9 meaning prediction error grows tenfold every ~2.5 time units. The reason weather is unforecastable beyond ~two weeks.
Lorenz system (sigma=10, beta=8/3, rho=28): toy weather that never repeats
Fixed points:
(+0.000, +0.000, +0.000)
(+8.485, +8.485, +27.000)
(-8.485, -8.485, +27.000)
Phase-space volume contraction rate div F = -13.667 (dissipative)
Largest Lyapunov exponent = 0.917 (positive -> chaos).
Prediction error grows e^(0.92 t): 10x every ~2.5 time units.
Two starts a millionth apart, separation over time:
t = 0.0 -> separation 1.000e-06
t = 8.0 -> separation 2.049e-06
t = 16.0 -> separation 5.507e-05
t = 24.0 -> separation 2.650e-02
t = 32.0 -> separation 2.086e+01
t = 40.0 -> separation 1.653e+01
A millionth of a degree in the initial state grows to opposite wings of the
butterfly -- why weather is unforecastable beyond ~two weeks. Deterministic, yet
unpredictable: the discovery that founded chaos theory.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lorenz.svgThe double pendulum: chaos you can hang from a nail
Hang one pendulum off another and you get the simplest chaotic mechanical system: fully deterministic, yet two nearly identical releases flail into totally different motions within seconds. Its coupled equations of motion have no closed form and are integrated numerically (RK4 here). Two things stay clean: the total energy is conserved (a stringent check the integrator passes over a well-resolved window), and the sensitive dependence on initial conditions is real -- a hair's difference in the start grows exponentially, the butterfly effect on a tabletop.
Double pendulum: deterministic yet chaotic (two rods, gravity)
Energy conservation (RK4, dt=1e-5, 0.05 s run):
E(0) = -27.676215 J, E(end) = -27.676315 J (relative drift 3.6e-06)
(RK4 is not symplectic, so energy slowly drifts over long chaotic runs; over a
short well-resolved window it holds to a part in a million or better.)
Two releases 1e-4 rad apart, angle-space separation over time:
t = 0.0 s -> separation 1.000e-04
t = 2.0 s -> separation 6.552e-04
t = 4.0 s -> separation 9.042e-04
t = 6.0 s -> separation 3.022e-03
t = 8.0 s -> separation 3.079e-03
t = 10.0 s -> separation 1.200e-03
A ten-thousandth of a radian grows until the two pendulums are doing utterly
different things -- the same sensitive dependence that makes weather chaotic, in a
system you can hang from a nail. Energy is conserved; the motion is still unpredictable.
wrote C:\Users\acwic\symplectic-nbody\examples\output\double_pendulum.svgThe Mandelbrot set: infinite detail from z -> z^2 + c
Iterate z -> z^2 + c from z=0: the Mandelbrot set is the c for which the orbit stays bounded. Since |z|>2 guarantees escape, the escape time -- how many steps to cross that radius -- colours the famous images, painting the filaments just outside the set. One quadratic rule generates a boundary of endless detail: the cardioid body, the period-2 bulb at c=-1, ever-smaller bulbs around the edge, and tiny copies of the whole set at every magnification. Its real slice is the logistic map's period-doubling route in complex dress.
Mandelbrot set: c stays bounded under z -> z^2 + c (escape when |z| > 2)
Escape time along the real axis (in-set = 100):
c = -2.50 -> escapes at 1
c = -2.00 -> IN SET
c = -1.00 -> IN SET
c = -0.50 -> IN SET
c = 0.00 -> IN SET
c = 0.25 -> IN SET
c = 0.35 -> escapes at 8
c = 0.50 -> escapes at 5
c = 1.00 -> escapes at 3
The set fills 24% of its [-2,0.5]x[-1.25,1.25] bounding box.
ASCII view (# = in set):
####
####
###############
#################
#################### #
## # ######################
######### #######################
##################################
##################################
######### #######################
## # ######################
#################### #
#################
###############
####
####
One quadratic rule, iterated, produces a fractal of endless detail -- the
cardioid body, the period-2 bulb at c=-1, ever-smaller bulbs around the edge, and
a boundary that carries tiny copies of the whole set at every magnification.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mandelbrot.svgThe Van der Pol oscillator: a self-sustaining rhythm
Unlike a pendulum that dies out, the Van der Pol oscillator x'' - mu(1-x^2)x' + x = 0 pumps itself: its damping is negative at small amplitude (energy in) and positive at large (energy out), so from almost any start it settles onto the same closed loop -- a limit cycle of amplitude ~2 that forgets its initial conditions. It is the model for self-regulated rhythms: heartbeats, firing neurons, bowed strings. Small mu gives near-sinusoidal oscillation; large mu gives relaxation oscillation -- slow charges broken by fast jumps, period ~1.6 mu.
Van der Pol: x'' - mu(1-x^2)x' + x = 0 -- damping that changes sign with amplitude
mu amplitude period character
0.1 1.41 6.29 near-sinusoidal
0.5 2.00 6.38 transitional
1.0 2.01 6.66 transitional
3.0 2.02 8.86 relaxation
5.0 2.02 11.61 relaxation
The amplitude settles near 2 for every mu -- the limit cycle forgets how it
started, whether launched tiny (grows in) or huge (decays in). That self-regulated
rhythm is the model for heartbeats, firing neurons, and bowed strings; at large mu
it becomes relaxation oscillation, slow charges broken by fast jumps (period ~1.6 mu).
wrote C:\Users\acwic\symplectic-nbody\examples\output\van_der_pol.svgThe Duffing oscillator: a spring that bends the rules
Add a cubic term to a spring -- x'' + delta x' + alpha x + beta x^3 = gamma cos(omega t) -- and it stops behaving linearly. Its resonance peak bends over with amplitude (the backbone sqrt(alpha + 3/4 beta A^2)), so the response is multi-valued and jumps between branches as you sweep the drive frequency (hysteresis). With alpha<0, beta>0 the potential is a double well -- a buckled beam or a bistable switch -- and a damped mass rolls into one of two stable states. Driven hard, the forced Duffing is one of the classic routes to chaos.
Duffing: x'' + delta x' + alpha x + beta x^3 = gamma cos(omega t)
alpha beta regime minima
1.0 1.0 hardening +0.00
1.0 -0.5 softening +0.00
-1.0 1.0 double-well -1.00, +1.00
1.0 0.0 linear +0.00
Backbone (amplitude-dependent resonance frequency, hardening spring):
amplitude 0.0 -> omega_res = 1.000
amplitude 0.5 -> omega_res = 1.090
amplitude 1.0 -> omega_res = 1.323
amplitude 1.5 -> omega_res = 1.639
The cubic term makes the spring's resonance bend: its peak frequency shifts with
amplitude, so the response is multi-valued and JUMPS between branches as you sweep
the drive (hysteresis). With alpha<0 the potential is a double well -- a buckled
beam -- and a damped mass rolls into one side; driven hard, the Duffing goes chaotic.
wrote C:\Users\acwic\symplectic-nbody\examples\output\duffing.svgThe Kuramoto model: oscillators falling into sync
A population of oscillators, each with its own natural frequency, pulls itself into step through coupling: dtheta_i/dt = omega_i + (K/N) sum sin(theta_j - theta_i). The synchrony is measured by the order parameter r (0 = phases scattered, 1 = all in phase), and there is a sharp phase transition -- below a critical coupling K_c the oscillators drift independently (r~0), above it a synchronized cluster spontaneously forms and r climbs toward 1. It is the canonical model of emergent collective order: fireflies flashing in unison, pacemaker cells, applause locking into rhythm, generators on a grid.
Kuramoto: dtheta_i/dt = omega_i + (K/N) sum sin(theta_j - theta_i)
Steady-state synchrony r vs coupling K (N=50, freq spread +/-1):
K order r state
0.0 0.100 incoherent
0.5 0.125 incoherent
1.0 0.198 incoherent
2.0 0.953 synchronized
4.0 0.989 synchronized
8.0 0.997 synchronized
Below a critical coupling the oscillators drift independently (r ~ 0); above it a
synchronized cluster spontaneously forms and r climbs toward 1 -- a phase
transition to collective order. Same mechanism: fireflies flashing in unison,
pacemaker cells, applause locking into rhythm, and generators on a power grid.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kuramoto.svgThe Abelian sandpile: self-organized criticality
Drop grains one at a time; wherever a pile reaches 4 it topples, one grain to each neighbour, and a single grain can set off an avalanche of any size. With no parameter tuning the pile drives itself to a critical state where avalanche sizes follow a power law -- mostly tiny, rarely system-spanning. The toppling is Abelian (the final state is independent of relaxation order). It is the founding model of self-organized criticality, a candidate for the scale-free statistics of earthquakes, forest fires, and neuronal avalanches.
Abelian sandpile: pile topples at 4 grains, sending one to each neighbour
Relaxing a central stack (no parameter tuning -> self-organized pattern):
100 grains -> 225 topples, 100 left on grid
500 grains -> 4702 topples, 500 left on grid
1000 grains -> 18182 topples, 872 left on grid
Avalanche statistics from 8000 random drops (20x20):
avalanches with topples: 1613 of 4000
mean size 16.8, max size 528
(mostly small, with rare system-spanning cascades -- a power-law tail)
With no tuning, the pile drives itself to a critical state where one grain can
trigger an avalanche of any size, scale-free like earthquakes, forest fires, and
neuronal cascades. The founding model of self-organized criticality.
wrote C:\Users\acwic\symplectic-nbody\examples\output\sandpile.svgElementary cellular automata: complexity from 8 bits
A row of 0/1 cells, each updated from itself and its two neighbours -- 2^8 = 256 possible rules, and from that trivial definition comes Wolfram's whole zoo: rule 0 dies to uniform, rule 90 draws the Sierpinski fractal by XOR, rule 30 is chaotic enough to have served as Mathematica's random-number generator, and rule 110 is Turing-complete -- a universal computer from an eight-bit lookup table. That computation needs almost no ingredients is one of the most surprising results in the field.
Elementary cellular automata: 256 rules, each an 8-bit lookup on 3 cells
rule table class / note
0 00000000 1: dies to uniform
90 01011010 2: Sierpinski fractal (XOR)
30 00011110 3: chaotic (used as an RNG)
110 01101110 4: complex, Turing-complete
184 10111000 2: traffic-flow model
Rule 90 from a single seed (the Sierpinski triangle):
#
# #
# #
# # # #
# #
# # # #
# # # #
# # # # # # # #
# #
# # # #
# # # #
# # # # # # # #
# # # #
# # # # # # # #
# # # # # # # #
# # # # # # # # # # # # # # # #
# #
# # # #
# # # #
# # # # # # # #
# # # #
From the simplest imaginable rule -- one output bit per 3-cell neighbourhood --
come fractals (90), chaos indistinguishable from random (30, once Mathematica's
RNG), and universal computation (110). Complexity needs almost no ingredients.
wrote C:\Users\acwic\symplectic-nbody\examples\output\cellular_automaton.svgConway's Game of Life: a universe from four rules
On a 2D grid, a live cell survives with 2-3 live neighbours and a dead cell is born with exactly 3 (B3/S23) -- and that is the whole rule. From it come still lifes (the block, unchanging), oscillators (the blinker, period 2), and spaceships (the glider, translating one cell diagonally every four generations). Because gliders can be fired from guns and collided to build logic gates, Life is Turing-complete: a computer can be built inside it. The canonical proof that simple local rules generate open-ended complexity.
Conway's Game of Life (B3/S23): live cell survives on 2-3, dead born on 3
Classic patterns:
block : still life (population 4, unchanged: True)
blinker : period-2 oscillator (returns in 2: True)
glider : spaceship, moves (1,1) every 4 gens
A glider crossing an 11x11 grid:
generation 0:
.#.........
..#........
###........
...........
...........
...........
...........
...........
...........
...........
...........
generation 4:
...........
..#........
...#.......
.###.......
...........
...........
...........
...........
...........
...........
...........
generation 8:
...........
...........
...#.......
....#......
..###......
...........
...........
...........
...........
...........
...........
generation 12:
...........
...........
...........
....#......
.....#.....
...###.....
...........
...........
...........
...........
...........
From B3/S23 come still lifes, blinking oscillators, gliders that fly, and -- via
glider guns and collisions building logic gates -- a Turing-complete computer.
Simple local rules, open-ended complexity.
wrote C:\Users\acwic\symplectic-nbody\examples\output\game_of_life.svgReaction-diffusion: Turing's spots and stripes
Turing showed in 1952 that patterns can form from chemistry alone: a slowly diffusing self-promoting activator and a fast-diffusing inhibitor make a uniform mixture unstable, and it settles into standing spots or stripes with no template. The Gray-Scott model du/dt = Du lap(u) - u v^2 + F(1-u), dv/dt = Dv lap(v) + u v^2 - (F+k)v produces, depending on the feed F and kill k rates, spots, stripes, mazes, self-replicating blobs, or waves -- a working model of morphogenesis behind leopard spots and seashell ridges.
Gray-Scott: du/dt = Du lap(u) - u v^2 + F(1-u), dv/dt = Dv lap(v) + u v^2 - (F+k)v
Seeding autocatalyst in a bare substrate and letting a pattern grow (F=0.035, k=0.06):
step total v contrast
0 16.0 0.250
200 86.5 0.373
600 192.3 0.354
1500 365.6 0.349
ASCII snapshot of the autocatalyst field (denser = more v):
.... ....
..:::........... ...........:::..
.:-==---::-------:. .:-------::---==-:.
.:=***+++==++++*++=:. .:=++*++++==+++***=:.
.-*###*********###+=:. .:=+###*********###*-.
.:=*###*******#####*+-....-+*#####*******###*=:.
.:=*##**+++++****###+-:..:-+###****+++++**##*=:.
.:-+***++=====++**##*=::::=*##**++=====++***+-:.
..-+**++-:::::-=+****+-::-+****+=-:::::-++**+-..
.-+**+=:......:-+***+=--=+***+-:......:=+**+-.
.:=**+=:. .:-=+++====+++=-:. .:=+**=:.
.:=**+=:. .:-=++++++++=-:. .:=+**=:.
.-+**+=:. .:-=++++++=-:. .:=+**+-.
.-+***=:. .:=++**++=:. .:=***+-.
.-+*#*+-. .:=+****+=:. .-+*#*+-.
.-+*#*+=:. :=**##**=: .:=+*#*+-.
.-*##**+-:. :=*####*=: .:-+**##*-.
.-+###**+-:. .-+####+-. .:-+**###+-.
.-+####**=-:.. ..:=*##*=:.. ..:-=**####+-.
:=+*##**+=-::::...:-=++=-:...::::-=+**##*+=:
.:=++***++=====-::..::::..::-=====++***++=:.
.:--=+++++++**+=-........-=+**+++++++=--:.
..::-==+++**##*=:. .:=*##**+++==-::..
..::-=++**####+:. .:+####**++=-::..
..::-=++**####+:. .:+####**++=-::..
..::-==+++**##*=:. .:=*##**+++==-::..
.:--=+++++++**+=-........-=+**+++++++=--:.
.:=++***++=====-::..::::..::-=====++***++=:.
:=+*##**+=-::::...:-=++=-:...::::-=+**##*+=:
.-+####**=-:.. ..:=*##*=:.. ..:-=**####+-.
.-+###**+-:. .-+####+-. .:-+**###+-.
.-*##**+-:. :=*####*=: .:-+**##*-.
.-+*#*+=:. :=**##**=: .:=+*#*+-.
.-+*#*+-. .:=+****+=:. .-+*#*+-.
.-+***=:. .:=++**++=:. .:=***+-.
.-+**+=:. .:-=++++++=-:. .:=+**+-.
.:=**+=:. .:-=++++++++=-:. .:=+**=:.
.:=**+=:. .:-=+++====+++=-:. .:=+**=:.
.-+**+=:......:-+***+=--=+***+-:......:=+**+-.
..-+**++-:::::-=+****+-::-+****+=-:::::-++**+-..
.:-+***++=====++**##*=::::=*##**++=====++***+-:.
.:=*##**+++++****###+-:..:-+###****+++++**##*=:.
.:=*###*******#####*+-....-+*#####*******###*=:.
.-*###*********###+=:. .:=+###*********###*-.
.:=***+++==++++*++=:. .:=++*++++==+++***=:.
.:-==---::-------:. .:-------::---==-:.
..:::........... ...........:::..
.... ....
Two chemicals -- a slow self-promoting activator and a fast inhibitor -- turn a
uniform mix unstable, and it settles into standing spots and stripes with no
template. Turing's model of morphogenesis: leopard spots, seashell ridges, and more.
wrote C:\Users\acwic\symplectic-nbody\examples\output\reaction_diffusion.svgBoids: flocking from three local rules
Reynolds showed that a flock needs no leader -- each boid just follows its neighbours by three rules: separation (avoid crowding), alignment (match heading), and cohesion (stay together). Sum those urges into an acceleration and a swarm of identical agents produces lifelike murmurations from a random scatter: the alignment (polarization) climbs toward 1 while separation keeps them from colliding, all bottom-up with no flock-level rule. The model behind starling murmurations, sardine bait balls, and the crowds in films and games.
Boids: separation + alignment + cohesion -> emergent flocking, no leader
step polarization mean spacing
0 0.146 7.90
30 0.282 9.04
80 0.337 8.28
200 0.727 8.54
From a random scatter (polarization near 0) the flock aligns into coherent motion
(polarization toward 1) while separation keeps the birds from colliding -- the
murmuration of starlings, the bait ball of sardines, all bottom-up from local rules.
wrote C:\Users\acwic\symplectic-nbody\examples\output\boids.svgDiffusion-limited aggregation: a fractal from random walkers
Release a particle far from a seed and let it random-walk until it touches the cluster, where it sticks; repeat. What grows is not a blob but a feathery, self-similar fractal, because a wanderer almost always brushes an outer tip long before it can diffuse into an interior fjord -- the tips screen the inside and grow faster still. The cluster's mass scales as N(r) ~ r^D with D ~ 1.71 in the plane, not 2: the branches leave most of the plane empty. The same instability draws mineral dendrites, electrodeposits, viscous fingers in a Hele-Shaw cell, lightning, and soot.
Diffusion-limited aggregation: random walkers stick to a growing seed
particles R_gyration fractal D
200 10.81 1.530
600 21.94 1.587
1200 31.81 1.609
Mass grows as N(r) ~ r^D with D ~ 1.7 in the plane, not 2: the branches
leave most of the plane empty. Outer tips screen the interior fjords -- a
wanderer brushes a tip long before it diffuses inside -- so the tips grow
faster and the cluster stays sparse and self-similar. Mineral dendrites,
electrodeposits, viscous fingers, lightning, and soot all grow this way.
wrote C:\Users\acwic\symplectic-nbody\examples\output\dla.svgBenford's law: why leading digits are not uniform
Count the leading digit of river areas, physical constants, stock prices, or file sizes and you do not get each of 1-9 a ninth of the time: 1 leads about 30% and 9 barely 4.6%, following P(d) = log10(1 + 1/d). The reason is scale invariance -- a quantity spanning many orders of magnitude is uniform in its logarithm, and a uniform log-mantissa maps to this logarithmic digit law, the only distribution invariant under a change of units. Multiplicative data (Fibonacci numbers, powers, factorials, populations) obey it, and departures flag fabricated accounting and election returns, which is why forensic auditors test for it.
Benford's law: P(d) = log10(1 + 1/d) -- 1 leads ~30% of the time, 9 only ~4.6%
digit Benford Fibonacci uniform
1 0.301 0.301 0.177
2 0.176 0.177 0.159
3 0.125 0.125 0.128
4 0.097 0.096 0.087
5 0.079 0.080 0.088
6 0.067 0.067 0.089
7 0.058 0.056 0.090
8 0.051 0.053 0.091
9 0.046 0.045 0.092
Fibonacci: chi2 = 0.17 TV dist = 0.0037 -> follows Benford
uniform: chi2 = 168.7 TV dist = 0.1509 -> does NOT
Multiplicative data (Fibonacci, powers, factorials, populations) spans many
orders of magnitude, so its mantissa is uniform in log space -- and that maps
to the logarithmic digit law. Forensic auditors flag fabricated ledgers and
election tallies by their departure from it.
wrote C:\Users\acwic\symplectic-nbody\examples\output\benford.svgThe coupon collector: how long to collect the whole set
Each cereal box holds one of n equally likely coupons; how many boxes to collect all n? Once you hold k of them a fresh box is new with probability (n-k)/n, so the wait for the next new one is geometric with mean n/(n-k), and summing gives E[T] = n H_n ~ n ln n. The last few coupons dominate: collecting the final one alone averages n boxes. The number needed is sharply concentrated, with a tail bound P(T > n ln n + c n) <= e^{-c}. The same law sets cache warmup, random test-coverage of n branches, and how many samples it takes to see every category at least once -- here the analytic E[T], variance, and completion CDF are checked against a seeded Monte-Carlo run.
Coupon collector: expected draws to collect all n coupons is E[T] = n * H_n
n E[T] n ln n+.. std dev sim mean
6 14.70 14.71 6.24 14.59
20 71.95 71.96 23.80 71.58
50 224.96 224.96 61.95 224.84
100 518.74 518.74 125.82 517.80
For n = 50: the first half of the coupons costs only 34 draws, but the last one alone
averages another 50 -- the tail dominates. Getting from 49 to 50 takes as long
as getting the first 35. That n ln n law sets cache warmup, random test coverage,
and how many samples it takes to see every category at least once.
wrote C:\Users\acwic\symplectic-nbody\examples\output\coupon_collector.svgThe secretary problem: optimal stopping and the 1/e rule
Interview n candidates one at a time in random order, accept or reject on the spot, and you only care about landing the single best. The optimal policy is a cutoff: reject the first r-1 (a look phase), then take the first later candidate who beats all seen so far. The win probability P(r) = (r-1)/n sum_{i=r}^{n} 1/(i-1) is maximized near r ~ n/e, and as n grows both the optimal look-fraction and the win probability tend to 1/e ~ 0.368: look at 37% of the field, then leap at the next record, and you land the very best about 37% of the time no matter how large n is. The same optimal-stopping law governs flat-hunting, parking, and online auctions -- here the exact probabilities are checked against a seeded Monte-Carlo run.
Secretary problem: reject the first ~37%, then take the next record. Win ~37%.
n r* (cutoff) look frac P(win) sim
10 4 0.3000 0.3987 0.4045
50 19 0.3600 0.3743 0.3712
100 38 0.3700 0.3710 0.3768
500 185 0.3680 0.3685 0.3550
1000 369 0.3680 0.3682 0.3643
Both the optimal look-fraction and the win probability tend to 1/e = 0.3679
as n grows: look at (and reject) 37% of the candidates, then leap at the next one
better than all of them, and you land the very best about 37% of the time -- no
matter how large n is. The same rule governs flat-hunting, parking, and auctions.
wrote C:\Users\acwic\symplectic-nbody\examples\output\secretary.svgThe birthday problem: coincidences are more common than they feel
How many people before two share a birthday with better-than-even odds? Only 23, not hundreds, because k people make k(k-1)/2 pairs and it is the pair count, growing like k^2, that drives collisions. The probability all k are distinct is prod (365-i)/365, so a collision passes 1/2 at 23 and 99.9% by 70. In general a collision becomes likely once k ~ 1.177 sqrt(d), a square-root law that sizes hash tables and UUID spaces and sets the birthday attack: a b-bit hash collides after ~2^(b/2) tries, not 2^b, which is why collision resistance needs twice the bits of preimage resistance. Exact and Poisson-approximate probabilities are checked against a seeded Monte-Carlo run.
Birthday problem: with 365 days, just 23 people make a shared birthday likelier than not
people k exact Poisson sim
10 0.1169 0.1160 0.1222
23 0.5073 0.5000 0.5147
40 0.8912 0.8820 0.8960
57 0.9901 0.9874 0.9880
70 0.9992 0.9987 0.9992
50% at k = 23, 99% at k = 57, 99.9% at k = 70.
The crossover grows only like ~1.177 sqrt(days) (~22.5); the first
collision arrives after ~24 draws. It is the number of
PAIRS, ~k^2/2, that drives collisions -- the same square-root law makes a b-bit hash
collide after ~2^(b/2) tries (the birthday attack), not 2^b.
wrote C:\Users\acwic\symplectic-nbody\examples\output\birthday.svgGambler's ruin: the walk that ends at a wall
Start with i dollars, bet $1 a round with win probability p, and stop at broke (0) or a target N -- a random walk with two absorbing walls. In a fair game the ruin chance is exactly 1 - i/N (your stake as a fraction of the table) and the game lasts i(N-i) rounds. But shift the odds a hair to p=0.49 and, starting at the halfway mark, the ruin chance leaps from 50% to 88%; against an infinitely rich house any p <= 1/2 is ruin with certainty. That asymmetry is why the house always wins, and the same absorbing-walk math models allele fixation in a finite population and sequential hypothesis tests. Exact ruin probabilities and durations are checked against a seeded Monte-Carlo run.
Gambler's ruin: bet $1 a round from i dollars, absorbed at 0 or N=100
p i ruin sim ruin duration sim dur
0.50 50 0.5000 0.5098 2500.0 2464.0
0.50 25 0.7500 0.7572 1875.0 1827.9
0.49 50 0.8808 0.8818 1904.1 1867.9
0.60 50 0.0000 0.0000 250.0 249.0
In a fair game the ruin chance is exactly 1 - i/N: your stake as a fraction of the
table. But shift the odds a hair to p=0.49 and starting at the halfway mark the ruin
chance leaps from 50% to 88% -- and against an infinitely rich house any p <= 1/2 is
ruin with certainty. That asymmetry is why the house always wins.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gamblers_ruin.svgParrondo's paradox: two losing games that together win
Two gambling games, each a sure loser played alone, can be alternated -- or chosen at random each round -- to make your capital drift UP. Game A is a slightly-losing flat coin; game B flips a terrible coin whenever your capital is a multiple of 3 and a good one otherwise, and loses because the walk gets stuck in the bad state too often. Mixing in game A reshuffles that state occupancy so the good coin comes up more, and the combined drift -- the stationary average of (2p-1) over the capital-mod-3 Markov chain -- turns positive. The same flashing-ratchet mechanism drives molecular motors, pumping directed motion from noise. Here the exact stationary drift is checked against a seeded Monte-Carlo trajectory.
Parrondo's paradox: game A loses, game B loses, but A-and-B-mixed WINS
game drift/round sim drift
A alone -0.01000 -0.00681
B alone -0.00870 -0.00763
50/50 mix 0.01570 0.01846
Game A is a slightly-losing flat coin. Game B flips a terrible coin whenever your
capital is a multiple of 3 and a good one otherwise -- and loses because the walk
gets stuck visiting the bad state too often. Mixing in game A reshuffles that
occupancy so the good coin comes up more, and the combined drift turns positive.
The same flashing-ratchet trick drives molecular motors: order pumped from noise.
wrote C:\Users\acwic\symplectic-nbody\examples\output\parrondo.svgThe Galton board: coin flips converge to a bell curve
Galton's bean machine is a board of n staggered peg rows; a bead bounces left or right with probability 1/2 at each row and lands in one of n+1 slots. Its slot is just the number of right-bounces in n coin flips, so the slot occupancy is the binomial C(n,k) p^k (1-p)^(n-k) -- and because it is a sum of n independent steps, the central limit theorem makes the histogram converge to a Gaussian of mean np and variance np(1-p) as n grows. It is the CLT made physical: no bead is steered, yet thousands pile into a smooth bell curve, and biasing the pegs slides the peak to np. The binomial-to-Gaussian distance shrinks like 1/sqrt(n), verified here against exact values and a seeded Monte-Carlo bead drop.
Galton board: 12 peg rows, each bead L/R by a coin flip -> binomial -> Gaussian
slot simulated binomial
0 0.0002 0.0002
1 0.0033 0.0029
2 0.0161 0.0161 ##
3 0.0547 0.0537 #######
4 0.1188 0.1208 ##############
5 0.1952 0.1934 #######################
6 0.2264 0.2256 ###########################
7 0.1940 0.1934 #######################
8 0.1189 0.1208 ##############
9 0.0541 0.0537 ######
10 0.0149 0.0161 ##
11 0.0033 0.0029
12 0.0002 0.0002
mean = np = 6.0, variance = np(1-p) = 3.00
binomial -> Gaussian distance shrinks like 1/sqrt(rows):
rows = 8: TV distance = 0.0139
rows = 16: TV distance = 0.0077
rows = 32: TV distance = 0.0037
rows = 64: TV distance = 0.0018
rows = 128: TV distance = 0.0009
No bead is steered, yet thousands pile into a smooth bell curve -- the central
limit theorem made physical. Bias the pegs and the whole pile slides to np.
wrote C:\Users\acwic\symplectic-nbody\examples\output\galton.svgThe Monty Hall problem: why switching doors wins
A car hides behind one of three doors; you pick one, and the host -- who knows where the car is -- opens a different door revealing a goat and offers a switch. Switching wins 2/3 of the time, staying only 1/3, because your first pick is right just 1/3 of the time and the host's forced reveal concentrates the whole remaining 2/3 onto the other closed door. The paradox lives in the host's knowledge: if he opened a door blindly and it happened to show a goat, switching would only be 1/2. Generalized to N doors with the host opening all but one other, switching wins (N-1)/N -- 99% at 100 doors. The exact stay and switch probabilities are checked against a seeded Monte-Carlo play.
Monty Hall: the knowing host's reveal shifts the odds -- switch and win 2/3
strategy theory sim
stay (classic) 0.333 0.333
switch (classic) 0.667 0.667
switch (random host) 0.500 0.498
Scaling up: N doors, the host opens all but one other goat door, you switch:
doors N stay 1/N switch
3 0.3333 0.6667
10 0.1000 0.9000
50 0.0200 0.9800
100 0.0100 0.9900
Your first pick is right only 1/N; the host's knowing reveal piles the whole
remaining (N-1)/N onto the last closed door. With 100 doors, switching wins 99%.
(If the host opened doors blindly, the effect vanishes -- it is his knowledge.)
wrote C:\Users\acwic\symplectic-nbody\examples\output\monty_hall.svgBayes and the base-rate fallacy: a positive test can still mean healthy
A disease affects 1 in 1000; a test is 99% sensitive and 99% specific; you test positive. The chance you are actually sick is not 99% but about 9%. Bayes' theorem combines the prior with the test's likelihoods, and the rare base rate makes false positives swamp the true ones: among 100,000 people, 99 true positives are buried under 999 false ones. A positive becomes more-likely-than-not only once the prevalence passes (1-spec)/(sens+1-spec) = 1%, and two independent positives push the posterior above 90%. This is the base-rate fallacy behind medical screening, spam filters, and security profiling -- here the posterior, likelihood ratios, and retest odds are checked against a seeded Monte-Carlo cohort.
Bayes & the base-rate fallacy: a 99%-accurate test, a 1-in-1000 disease prevalence = 0.100% sensitivity = 99% specificity = 99% P(sick | positive) = 9.0% <- not 99%! (Monte-Carlo: 9.1%) P(sick | 2 positives)= 90.7% break-even prevalence= 1.00% (a positive is 50-50 here) In 100,000 people: 100 are sick and 99 test positive, but 99,900 are healthy and 1% of them -- 999 -- test positive too. So 99 of 1098 positives are real: 9%. The rare disease lets false positives swamp true ones however good the test sounds -- the base-rate fallacy behind medical screening, spam filters, and profiling. wrote C:\Users\acwic\symplectic-nbody\examples\output\bayes_test.svg
Shannon entropy and Huffman coding: the limit of lossless compression
How few bits, on average, to record one symbol from a source? Shannon's entropy H = -sum p log2 p is the answer, and no lossless code can beat it: the average codeword length L obeys L >= H, with a code always existing at L < H+1. Entropy is maximal (log2 n) for a uniform source and zero when one symbol is certain -- it measures surprise. Huffman's algorithm merges the two least-likely symbols repeatedly to build the optimal prefix code, provably landing in the [H, H+1) band and obeying the Kraft inequality sum 2^-len <= 1. This module computes entropy, builds the Huffman code, verifies the Shannon bound and a lossless encode/decode round-trip -- the Huffman stage inside ZIP, JPEG, and MP3.
Shannon entropy & Huffman coding: compressing a skewed source
symbol prob codeword bits
e 0.27 10 2
t 0.20 00 2
a 0.16 111 3
o 0.13 110 3
n 0.12 010 3
s 0.07 0111 4
z 0.05 0110 4
entropy H = 2.6318 bits/symbol
Huffman L = 2.6500 bits/symbol (fixed-length would need 3)
bound: H <= L < H+1 -> 2.6318 <= 2.6500 < 3.6318 OK
efficiency H/L = 99.3%, Kraft sum = 1.000
'tenants' -> 0010010111010000111 (19 bits vs 56 raw), decodes back: ok
No lossless code can beat the entropy; Huffman lands within one bit of it and is
optimal among prefix codes -- the reason ZIP, JPEG, and MP3 all carry a Huffman stage.
wrote C:\Users\acwic\symplectic-nbody\examples\output\shannon.svgThe Kelly criterion: how much to bet to grow fastest
Given an edge -- a bet paying b-to-1 that wins with probability p above break-even -- how much of your bankroll should you stake? Kelly's answer maximizes long-run compound growth and is a fixed fraction f* = p - (1-p)/b, the edge over the odds. The growth rate g(f) = p ln(1+bf) + (1-p) ln(1-f) is a concave curve peaking at f*: betting less is safe but slow, betting past 2f* drives the growth rate negative and you go broke despite a winning edge. Half-Kelly keeps about 3/4 of the growth at far less volatility, which is why traders bet fractional Kelly. The same log-optimal rule (which Kelly derived from Shannon's channel capacity) sizes positions in quantitative finance -- here it is checked against a seeded Monte-Carlo of the compounding bankroll.
Kelly criterion: even-money bet, win probability p=0.6, edge 20%
optimal fraction f* = 0.200 (bet 20% of bankroll each time)
growth rate g(f*) = 0.0201 per bet
Monte-Carlo growth = 0.0204
doubling time = 34.4 bets
break-even fraction = 0.389 (overbetting past this loses)
fraction growth vs f*
0.05 0.0088 0.43
0.10 0.0150 0.75
0.20 0.0201 1.00 <- Kelly
0.30 0.0147 0.73
0.40 -0.0024 -0.12
0.50 -0.0340 -1.69
Bet the Kelly fraction and the bankroll compounds fastest in the long run. Bet
more and volatility eats the growth -- past 2f* the growth rate goes negative and
you go broke despite a winning edge. Half-Kelly keeps ~3/4 the growth at far less
risk, which is why real traders and gamblers bet fractional Kelly.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kelly.svgHamming codes: correcting a bit error from the syndrome
Bits flip on a noisy channel. A parity bit can detect a single error but not fix it; Hamming's 1950 codes correct it. Placing parity bits at the power-of-two positions so each data bit is covered by a unique combination of checks, the pattern of failed checks -- the syndrome -- reads out, in binary, the exact position of the flipped bit. The classic Hamming(7,4) carries 4 data bits in 7 and corrects any single error; Hamming(2^m-1, 2^m-1-m) needs only m parity bits, so the overhead shrinks as blocks grow. Every Hamming code has minimum distance 3, and one extra overall parity bit gives SECDED (single-correct, double-detect), the scheme in ECC memory. Verified by exhaustively correcting every single-bit error in every codeword.
Hamming(7,4): 4 data bits -> 7 transmitted bits, corrects any single error
data [1, 0, 1, 1] -> codeword [0, 1, 1, 0, 0, 1, 1] (parity bits at positions 1,2,4)
flipped pos received syndrome corrected?
1[1, 1, 1, 0, 0, 1, 1] 1 yes
2[0, 0, 1, 0, 0, 1, 1] 2 yes
3[0, 1, 0, 0, 0, 1, 1] 3 yes
4[0, 1, 1, 1, 0, 1, 1] 4 yes
5[0, 1, 1, 0, 1, 1, 1] 5 yes
6[0, 1, 1, 0, 0, 0, 1] 6 yes
7[0, 1, 1, 0, 0, 1, 0] 7 yes
The failed parity checks spell out, in binary, the exact position of the flipped
bit -- flip it back and the message is restored. No retransmission needed.
code data k total n rate k/n
(3,1) 1 3 0.333
(7,4) 4 7 0.571
(15,11) 11 15 0.733
(31,26) 26 31 0.839
(63,57) 57 63 0.905
More parity bits per block -> higher rate: the (255,247) code spends just 8 bits
to protect 247. Add one overall parity bit for SECDED -- the scheme in ECC memory.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hamming.svgRSA: public-key cryptography from the hardness of factoring
RSA lets two strangers communicate secretly without ever sharing a key, resting on one asymmetry: multiplying two large primes into n = pq is easy, but factoring n back apart is (as far as anyone knows) astronomically hard. Pick primes p, q, take phi = (p-1)(q-1), a public exponent e coprime to phi, and the private d = e^-1 mod phi. Encryption is c = m^e mod n, decryption m = c^d mod n, and they undo each other because ed = 1 mod phi (Euler). The same keys sign: encrypt with the private key, verify with the public. This pure-stdlib reference implements Miller-Rabin primality (catching Carmichael numbers that fool Fermat), the extended Euclidean inverse, fast square-and-multiply exponentiation, and full encrypt/decrypt/sign/verify round-trips -- the number theory behind TLS and SSH.
RSA from scratch: two primes -> a public key anyone can encrypt to
public key (e, n): e = 65537
n = 88411744846329904466854580393080427256266851130989221887466523070287323859577
private key (d, n): d = 35466145414193710246633305133498396822429053497000535443257497919929133786793
message m = 42424242
encrypt c = m^e mod n = 84839668179731333482683177734528306802513032577352587067916241783443511071227
decrypt m'= c^d mod n = 42424242 -> round-trip OK
sign s = m^d mod n = 26946381523425824622003667727963989899574990650435005788254097895555661275214
verify s^e mod n = m ? True (tampered: False)
bytes: b'public-key crypto, no shared secret'
-> 2 ciphertext blocks -> decrypts to b'public-key crypto, no shared secret' (OK)
Multiplying the primes is easy; factoring n back apart is not -- that gap is the
whole of RSA's security. Modular exponentiation costs ~log2(e) multiplications by
square-and-multiply, so even a 2048-bit exponent is fast.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rsa.svgDiffie-Hellman: agreeing on a secret in the open
Two people who have never met, on a wiretapped line, can agree on a shared secret the eavesdropper cannot learn. Fix a prime p and generator g; Alice sends g^a mod p, Bob sends g^b mod p, and each raises what they received to their own secret, both landing on g^(ab) mod p while the wire carried only g^a and g^b. Stealing the secret means recovering a from g^a mod p -- the discrete-logarithm problem, easy to state and (for large p) astronomically hard. This module generates safe-prime parameters, finds a generator, runs the exchange, and includes a baby-step/giant-step discrete-log solver whose sqrt(p) cost dwarfs the parties' log(p) work -- the gap that keeps the secret safe. Without authentication a man-in-the-middle can still intercept, which is why real protocols sign the exchange.
Diffie-Hellman: agree on a secret while an eavesdropper listens
public parameters: p = 48563 (safe prime), g = 2 (generator)
secret sends g^secret mod p computes
Alice 12345 30204 B^a mod p = 9050
Bob 54321 20462 A^b mod p = 9050
shared secret g^(ab) mod p = 9050 (both match: True)
the wire carried only p, g, 30204, 20462 -- never a or b.
An eavesdropper must solve g^x = 30204 (mod 48563) for x. Baby-step/giant-step finds
x = 12345 in ~sqrt(p) = 220 steps -- feasible ONLY because p is tiny here.
Scale p to 2048 bits and sqrt(p) is ~10^308 steps: the secret is safe, even though
Alice and Bob each did only a few thousand multiplications. That gap is the point.
wrote C:\Users\acwic\symplectic-nbody\examples\output\diffie_hellman.svgCRC: catching transmission errors with polynomial division
A cyclic redundancy check is the checksum on almost every digital frame -- Ethernet packets, ZIP files, PNG chunks, disk sectors. Treat the message as a polynomial over GF(2) (arithmetic mod 2, addition = XOR), divide by a fixed generator, and append the remainder; the receiver divides again and a nonzero remainder flags corruption. A degree-r generator guarantees detection of every single-bit error, every burst shorter than r+1 bits, and misses a random error only with probability ~2^-r -- 1 in 4 billion for CRC-32, computed with nothing but shifts and XORs. This module does bit-at-a-time polynomial division for CRC-8/16/32, reproducing the published '123456789' check values and matching zlib.crc32 exactly. The detection companion to the Hamming code (which corrects).
CRC: divide the message polynomial by a generator, send the remainder
check string '123456789':
CRC-8 = 0xF4
CRC-16-CCITT = 0x29B1
CRC-32 = 0xCBF43926
Frame = data + CRC. Receiver recomputes and compares:
data : b'GET /index.html'
frame : ...139492ad (4 CRC bytes appended), check = OK
corrupt one bit -> check = ERROR CAUGHT
Error-detection coverage (random multi-bit corruptions):
CRC width caught miss ~2^-r
CRC-8 8 0.9960 3.91e-03
CRC-16-CCITT 16 0.9988 1.53e-05
CRC-32 32 0.9988 2.33e-10
Every single-bit error, every burst shorter than the width, and all but a
~2^-r fraction of random corruptions are caught -- for 32 check bits that is a
miss rate of 1 in 4 billion, with nothing but shifts and XORs. The checksum on
Ethernet frames, ZIP files, and PNG chunks.
wrote C:\Users\acwic\symplectic-nbody\examples\output\crc.svgLZ77: compression by pointing back at what you have seen
Lempel and Ziv's 1977 algorithm is the engine inside ZIP, gzip, and PNG. Scanning the data, whenever the next bytes have appeared recently it emits a back-reference -- a (distance, length) pair meaning 'copy length bytes from distance back' -- instead of repeating them; only genuinely new bytes are stored literally. A sliding window holds the recent history, and the decompressor replays the tokens from its own growing output, so an overlapping copy expands a whole run from one token. Repetitive data (text, code, logs) compresses enormously while random data cannot shrink at all -- Shannon's entropy limit showing through. This module implements the encoder and decoder with a guaranteed lossless round-trip; LZ77 plus Huffman together are DEFLATE.
LZ77: replace repeats with (distance, length) back-references
input (46 bytes): the cat sat on the mat, the cat sat on the hat
token stream:
the cat s[<-4,3]on [<-15,4]mat,[<-9,5]c[<-24,14]hat
16 literals + 4 back-references, round-trip: OK
compression ratio ~ 1.44x
Ratio climbs with repetition, but incompressible data stays ~1 (Shannon's limit):
data bytes ratio
one 'ab' pair 2 1.00
'ab' x 5 10 2.00
'ab' x 50 100 20.00
English text x 20 400 14.81
pseudo-random bytes 400 1.00
LZ77 + Huffman together are DEFLATE -- the algorithm inside gzip, ZIP, and PNG.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lz77.svgBloom filters: membership in a fraction of the space
A Bloom filter answers 'have I seen this?' with a bit array and a few hash functions, in a tiny fraction of the memory the items would take. The trade is one-sided: it may say 'possibly present' for something never added (a false positive) but NEVER says 'absent' for something you did add -- no false negatives. Add an item by setting its k bits; test by checking all k are set. After n items in m bits the false-positive rate is (1 - e^{-kn/m})^k, minimized at k = (m/n) ln 2, needing only ~1.44 log2(1/p) bits per item regardless of item size -- a million URLs at 1% error in about 1.2 MB. Web caches, spell checkers, and databases use one as a fast pre-filter. Verified here: zero false negatives and an observed false-positive rate matching theory.
Bloom filter: 'possibly present' or 'definitely absent', never a false negative
1000 items at 1% target: 9586 bits (9.6 bits/item), 7 hash functions
vs storing the items themselves: a fraction of the memory, item-size-independent
after inserting 1000: false negatives = 0 (guaranteed 0)
observed false-positive rate = 0.0103, theory = 0.0100
fill ratio = 0.513 (optimal load sets ~half the bits)
False-positive rate as the filter fills (fixed m, k):
items theory observed
250 0.0000 0.0000
500 0.0003 0.0011
1000 0.0100 0.0076
2000 0.1574 0.1104
4000 0.6786 0.5940
Web caches, spell checkers, and databases use one as a fast pre-filter: skip the
expensive lookup for anything the filter says is definitely not there.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bloom.svgHyperLogLog: counting distinct items in kilobytes
How many DISTINCT items in a stream, when exact counting means storing every one? HyperLogLog estimates the cardinality to a percent or two in fixed tiny memory -- a billion distinct items in ~1.5 KB. Hash each item; the longest run of leading zeros seen hints at the count (k zeros suggests ~2^k items). To tame the noise, the first p bits pick one of m = 2^p registers each holding its max leading-zero rank, and the harmonic mean across registers gives E = alpha_m m^2 / sum 2^-M[j] with relative error ~1.04/sqrt(m). Small counts get a linear-counting correction, and two sketches merge by register-wise max, so counts are trivially distributed -- which is why Redis, Presto, and BigQuery all ship it. Verified against true cardinalities across four orders of magnitude.
HyperLogLog: distinct-count with 4096 registers = 4096 bytes, standard error 1.62%
true distinct estimate rel error
100 98 -0.0183
1000 998 -0.0016
10000 9760 -0.0240
100000 99707 -0.0029
500000 496746 -0.0065
Memory stays fixed at ~4096 bytes whether you count 100 or 500,000 items --
exact counting would need a set holding all of them. A billion distinct items
fit in ~1.5 KB. Sketches merge by register-wise max, so counts are distributed.
merge: |A|~59700 (true 60000), |B|~59599 (true 60000)
|A union B|~98472 (true 100000) -- counted without a shared list.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hyperloglog.svgFenwick trees: running sums that update in log time
Keeping an array while asking for prefix sums, a plain array gives instant updates but O(n) sums, and a prefix-sum array the reverse. Fenwick's binary indexed tree does both in O(log n) using the binary structure of the indices: node i stores the partial sum of the range ending at i whose length is its lowest set bit i & -i. A prefix sum strips the low bit each step (i -= i & -i); an update adds it (i += i & -i) -- each walk touches only one node per set bit. Range sums come by subtraction, and because cumulative sums are monotone you can binary-search the tree for the smallest index whose prefix reaches a target, an O(log n) 'select' for weighted sampling and rank queries. Every operation is cross-checked here against a brute-force array over thousands of mixed updates and queries.
Fenwick tree (binary indexed tree): update AND prefix-sum in O(log n)
values : [3, 1, 4, 1, 5, 9, 2, 6]
prefix sums : [3, 4, 8, 9, 14, 23, 25, 31]
total = 31, range_sum[2,6) = 19 (= 19)
after adding 10 at index 3: value there = 11, new total = 41
cumulative search over weights [2, 0, 3, 1, 4, 0, 5] (prefix sums [2, 2, 5, 6, 10, 10, 15]):
smallest index whose prefix sum >= 1: 0
smallest index whose prefix sum >= 3: 2
smallest index whose prefix sum >= 6: 3
smallest index whose prefix sum >= 12: 6
-- the O(log n) 'select' used for weighted sampling and rank queries.
n Fenwick log2 n naive prefix-array n
16 4 16
1,024 10 1,024
1,000,000 20 1,000,000
1,000,000,000 30 1,000,000,000
Each update or query touches one node per set bit of the index -- log n nodes --
in a single array of n integers. It powers range queries, order statistics, and
streaming quantiles.
wrote C:\Users\acwic\symplectic-nbody\examples\output\fenwick.svgUnion-Find: connectivity in near-constant time, and Kruskal's MST
Given a stream of 'these two are connected' facts, answer whether any two items are in the same group -- the disjoint-set problem. Union-Find solves a sequence of m operations in O(m alpha(n)) time, where the inverse Ackermann alpha(n) is at most 4 for any conceivable n: effectively constant. Each set is a tree with a representative root; union by rank hangs the shorter tree under the taller, and path compression repoints every node visited during a find straight at the root, so trees stay flat. A cycle in a graph is exactly two endpoints already in the same set, which makes Union-Find the whole of Kruskal's minimum-spanning-tree algorithm. It also drives image segmentation, percolation, and account-merging. Verified against a brute-force flood fill and known minimum spanning trees.
Union-Find: merge groups and query connectivity in ~O(1) amortized
start: 10 singletons
connect 0-1: merge -> 9 components
connect 2-3: merge -> 8 components
connect 4-5: merge -> 7 components
connect 1-2: merge -> 6 components
connect 6-7: merge -> 5 components
connect 5-8: merge -> 4 components
connect 3-6: merge -> 3 components
final components: [[0, 1, 2, 3, 6, 7], [4, 5, 8], [9]]
connected(0, 8)? False connected(0, 9)? False
size of 0's group: 6
Kruskal's MST (add cheapest edge that joins two components):
weight edge
5 0--3
5 2--4
6 3--5
7 0--1
7 1--4
9 4--6
total spanning-tree weight = 39 (6 edges for 7 nodes)
A cycle is exactly two endpoints already in the same set, so Union-Find spots it
in constant time -- which is the whole of Kruskal. It also drives image
segmentation, percolation, and 'friend circle' / account-merge problems.
wrote C:\Users\acwic\symplectic-nbody\examples\output\union_find.svgDijkstra's algorithm: shortest paths, greedily
Given a graph with nonnegative edge weights, Dijkstra finds the cheapest route from a source to every node by a greedy rule: keep a tentative distance to each node, repeatedly settle the unsettled node with the smallest distance, and relax its edges. Once settled, a node's distance is final -- which holds precisely because no nonnegative detour can improve a shorter path. With a binary-heap priority queue it runs in O((V+E) log V). This module builds a weighted graph, runs Dijkstra (returning distances and a predecessor tree for path reconstruction), and includes a from-scratch binary min-heap and a Bellman-Ford implementation used to verify every distance. Add a goal heuristic to the priority and it becomes A*, the workhorse of map and game routing -- shown here solving a grid maze.
Dijkstra: cheapest route from a source to every node
node distance Bellman-Ford
0 0 0
1 3 3
2 1 1
3 4 4
4 7 7
shortest 0 -> 4: [0, 2, 1, 3, 4], cost 7 (Dijkstra == Bellman-Ford: True)
Grid maze (10x10): shortest S->G path length = 32
'#' walls, '*' the path Dijkstra found:
S****#****
.###*#*##*
.#***#*#**
.#*###*#*#
.#*****#*#
.#####.#*#
.....#.#**
####.#.##*
...#...#.*
.#.####..G
wrote C:\Users\acwic\symplectic-nbody\examples\output\dijkstra.svgk-d trees: fast nearest-neighbour search in space
"Which point is closest to this query?" is asked constantly in graphics, robotics, machine learning, and geographic search, and scanning every point is O(n). A k-d tree organizes the points by recursive median splitting -- the root splits on x, the next level on y, then z, cycling axes -- so a query descends to its leaf and then unwinds, only crossing a splitting plane into the far subtree when that hyper-rectangle could hold something nearer. Whole branches are pruned, giving O(log n) typical queries. The same descent-and-prune serves k-nearest-neighbours and radius queries. This module builds a balanced tree and does exact nearest, k-nearest, and radius search, each cross-checked against a brute-force scan in 2D and 3D. It powers k-NN classification, particle neighbour lists, and map search.
k-d tree: nearest-neighbour search by recursive median splitting 400 points in 2D, tree height 9 (~log2 n = 8.6) nearest to (50.0, 50.0): 49.2,47.8 (brute force agrees: True) 5 nearest agree with brute force: True within radius 15: 32 points (brute force agrees: True) nearest matches brute force on 500 random queries: True Each query descends to a leaf, then unwinds -- crossing the splitting plane only when the far side could hold something closer, so most branches are pruned. It powers k-NN classification, particle neighbour lists, and map/geographic search. wrote C:\Users\acwic\symplectic-nbody\examples\output\kdtree.svg
Boyer-Moore: the string search that skips ahead
Finding a pattern in text is one of computing's most-run operations. The naive scan checks every position in O(n*m); Boyer-Moore -- the algorithm your editor's find actually uses -- matches the pattern right-to-left and, on a mismatch, jumps forward by more than one position, often by nearly the whole pattern. Two precomputed rules set the skip: the bad-character rule shifts so the last occurrence of the mismatched text character lines up (or past it entirely if absent), and the good-suffix rule realigns an already-matched suffix without undoing confirmed matches. Taking the larger shift keeps it safe and makes it sublinear on real text -- most characters are never examined. This module builds both tables and finds first/all/overlapping occurrences, proven correct exhaustively against a naive search over 5000 random cases.
Boyer-Moore: match right-to-left and jump ahead on a mismatch
text : GCATCGCAGAGAGTATACAGTACG
pattern : GCAGAGAG
found at: [5] (naive agrees: True)
bad-character table (last index of each pattern character):
G:7 C:1 A:6
Character comparisons, Boyer-Moore vs naive (English text, longer patterns win):
pattern BM comps naive speedup
fox 1079 2818 2.6x
lazy dog 781 3113 4.0x
jumps over the 1255 3467 2.8x
the quick brown fox 1559 4352 2.8x
A longer pattern with a rare mismatch character leaps forward by nearly its whole
length, so most of the text is never even examined -- the search is sublinear.
wrote C:\Users\acwic\symplectic-nbody\examples\output\boyer_moore.svgA* search: Dijkstra with a sense of direction
Dijkstra explores outward in every direction equally; A* keeps the same guarantee of an optimal path but adds a heuristic h(n) estimating the remaining distance to the goal, ordering its frontier by f(n) = g(n) + h(n) -- known cost so far plus the guess ahead -- so it pushes toward the goal instead of flooding. If the heuristic never overestimates the true remaining cost (it is admissible), the path is still guaranteed optimal; with h = 0 it degenerates exactly to Dijkstra. On a grid the Manhattan distance is admissible for 4-directional movement and the octile distance for 8-directional. This module runs A* on a weighted grid with obstacles, returns the path and the expanded-node set, and verifies A* finds the same optimal cost as Dijkstra while expanding no more nodes -- the standard for game and robot navigation.
A*: guide the search with a heuristic h(n) = estimated distance to the goal
grid 13x30, start (0, 0) -> goal (0, 29)
path cost nodes expanded
A* 49 300
Dijkstra 49 360
same optimal cost: True A* expanded 17% fewer nodes
A* orders its frontier by f = g + h: known cost so far plus the guess to the goal,
so it pushes toward the goal instead of flooding outward. With an admissible
heuristic (never overestimating) the path it returns is still guaranteed optimal --
which is why A* is the standard for game and robot navigation.
wrote C:\Users\acwic\symplectic-nbody\examples\output\astar.svgTopological sort: ordering tasks so prerequisites come first
A directed acyclic graph encodes dependencies -- an edge u -> v means u must come before v -- and a topological order is a linear arrangement in which every edge points forward: the order you can build the modules, install the packages, or recompute the spreadsheet cells. It exists exactly when the graph has no cycle. Kahn's algorithm repeatedly emits a node with no remaining incoming edges (BFS on in-degrees); the DFS method reverses finish-times and spots a cycle as a back-edge to a node still on the stack. Both run in O(V+E). With a duration on each task, the longest path through the DAG is the critical path -- the minimum time to finish everything. This module implements both sorts, cycle detection, and the critical path, and verifies every order it returns respects all edges across hundreds of random DAGs.
Topological sort: a build order where every dependency comes first
Kahn: fetch -> compile -> link -> test -> assets -> package -> deploy
DFS: assets -> fetch -> compile -> test -> link -> package -> deploy
(both are valid orders; ties resolved differently)
a -> b -> c -> a has a cycle: True (no order can exist)
Project critical path (longest chain of durations = minimum completion time):
design -> backend -> integrate -> qa -> release = 22 days
(total task-days = 35, but the critical path bounds the schedule)
The same ordering runs package managers, build systems, spreadsheet recompute,
and course prerequisites -- and a cycle is a dependency that can never be resolved.
wrote C:\Users\acwic\symplectic-nbody\examples\output\toposort.svgLevenshtein edit distance: how far apart are two strings
The edit distance is the fewest single-character edits -- insert, delete, substitute -- that turn one string into another, the measure behind spell-checkers, fuzzy search, diff, and DNA alignment. Dynamic programming fills a table where d[i][j] is the distance between prefixes, each cell the cheapest of a match, substitution, insertion, or deletion, in O(mn); backtracing the choices recovers the actual alignment, not just the count. The distance is a true metric (symmetric, zero only for equal strings, triangle-inequality-respecting). This module computes the distance, a memory-lean two-row variant, the alignment operations, a similarity ratio, and the Damerau variant that treats an adjacent-character swap as one edit (the commonest typo), all verified against known values and the metric axioms.
Levenshtein edit distance: fewest insert/delete/substitute edits
'kitten' -> 'sitting' : distance 3, similarity 0.57
alignment:
substitute k -> s
keep i
keep t
keep t
substitute e -> i
keep n
insert g
applying the ops reproduces 'sitting'
spell-check 'recieve' against a small dictionary (nearest first):
word distance damerau
relieve 1 1
believe 2 2
recede 2 2
receive 2 1
deceive 3 2
receiver 3 2
receipt 4 3
best guess: 'relieve' -- and Damerau sees 'recieve'->'receive' as a single
adjacent-swap typo (distance 1), the commonest kind of mistake. The same DP powers
fuzzy search, diff tools, and DNA sequence alignment.
wrote C:\Users\acwic\symplectic-nbody\examples\output\levenshtein.svgThe 0/1 knapsack: packing the most value under a weight limit
Items each have a weight and a value; which subset maximizes value without exceeding a capacity W, taking each item whole or not at all? Brute force checks 2^n subsets, but dynamic programming solves it in pseudo-polynomial O(nW): best[i][w] is the most value from the first i items within capacity w, each cell the better of skipping item i or taking it (freeing w - weight_i of room). Backtracing recovers which items to take; a rolling array (iterating capacity downward so each item is used once) cuts memory to O(W). The related subset-sum question and the unbounded knapsack (unlimited copies, iterate capacity upward) are the same table. This module solves all four, reconstructs the chosen items, and is verified exhaustively against a brute-force subset search -- the model for budget allocation, cargo loading, and portfolio selection under a hard cap.
0/1 knapsack: pick items to maximize value within 8 kg
item weight value
camera 2 6 <- take
laptop 3 8
whisky 4 7
book 1 2
gold bar 5 12 <- take
phone 1 5 <- take
optimal value 23 using 8 kg (brute force agrees: True)
Optimal value as the bag grows:
capacity best value
0 0
2 7
4 13
6 19
8 23
10 27
12 33
14 34
16 40
Each cell of the DP table is 'skip item i' vs 'take it and free up its weight' --
O(nW) instead of the 2^n subsets brute force checks. It models budget allocation,
cargo loading, and portfolio selection under a hard cap.
wrote C:\Users\acwic\symplectic-nbody\examples\output\knapsack.svgLongest common subsequence: the engine behind diff
A subsequence keeps some elements in order but may skip others; the longest common subsequence of two sequences is the longest ordering appearing in both, and it is what diff, git, patch, and bioinformatics comparison are built on -- the complement of the LCS is exactly the lines to add or delete, so a bigger LCS means a smaller diff. The dynamic program fills L[i][j] = L[i-1][j-1]+1 on a match, else max(L[i-1][j], L[i][j-1]), in O(mn); backtracing recovers an actual longest subsequence, and turning the walk into keep/delete/insert steps yields the diff. The LCS length also gives the insert/delete edit distance, m + n - 2*LCS. This module computes the length, one subsequence, the diff edit-script, and that distance, all verified exhaustively against a brute-force subsequence search.
Longest common subsequence: the longest in-order shared thread
a = ABCBDAB
b = BDCAB
LCS = 'BCAB' (length 4), indel edit distance 4
A line-level diff (LCS of the two files' lines):
import os
+ import sys
def main():
- x = 1
+ x = 2
return x
- main()
patch reproduces v2: True
The kept lines are the LCS; the +/- lines are its complement -- the smallest set
of edits. A bigger LCS means a smaller diff. This runs git, patch, and
bioinformatics sequence comparison.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lcs.svgQuickselect: the k-th smallest without sorting
Finding the median, a percentile, or the top-k threshold looks like it needs a full O(n log n) sort -- it does not. Quickselect partitions the array around a pivot and recurses only into the side containing rank k, discarding half the data each step for O(n) expected time; you never touch the elements you do not need. A bad pivot would degrade it to O(n^2), so the median-of-medians algorithm (medians of groups of five, recursively) picks a pivot provably better than 30% and worse than 30% of the data, guaranteeing worst-case linear time -- the classic proof that selection beats sorting. This module implements quickselect with a randomized pivot and with median-of-medians, plus median, k-th smallest/largest, and percentile wrappers, each verified against a full sort over 1000 random arrays.
Quickselect: order statistics in O(n), no full sort
data: [37, 12, 91, 5, 68, 24, 50, 3, 79, 45, 18, 60]
min = 3
median = 41.0
90th pct = 79
3rd largest = 68
(median-of-medians agrees: True)
Comparisons to find the median: quickselect (~2n) vs a full sort (~n log n):
n quickselect sort ~n log n ratio
64 238 384 1.6x
256 825 2048 2.5x
1024 3491 10240 2.9x
4096 13248 49152 3.7x
16384 61484 229376 3.7x
Discarding one partition side each step gives O(n) expected time. The
median-of-medians pivot (groups of five) guarantees it even in the worst case --
the theoretical proof that selection beats sorting when you need just one rank.
wrote C:\Users\acwic\symplectic-nbody\examples\output\quickselect.svgAho-Corasick: finding many patterns in one pass
A spam filter or virus scanner must match hundreds of patterns at once; running a single-pattern search per pattern costs O(n * patterns). The Aho-Corasick automaton finds every occurrence of every pattern in a single left-to-right scan, in O(n + total pattern length + matches) -- independent of the pattern count. It builds a trie of the patterns, then adds failure links (fall back to the longest proper suffix that is also a pattern prefix, so no character is re-examined -- KMP generalized to many patterns) and output links (report every pattern ending at the current state, catching overlapping and nested matches like 'he', 'she', 'hers' in 'ushers'). Built once by breadth-first traversal, then one text pass reports all matches. This module builds the automaton and finds all matches, verified exhaustively against a brute-force per-pattern search.
Aho-Corasick: match a whole dictionary of patterns in one left-to-right scan
patterns ['he', 'she', 'his', 'hers'] in 'ushers':
'he' at [2]
'she' at [1]
'hers' at [2]
overlapping/nested matches all caught; brute force agrees: True
scanning a log line for 6 patterns in one pass:
'error' at [28]
'fail' at [60]
'warn' at [15]
'timeout' at [35]
'critical' at [50]
'denied' at [73]
total hits: 6, contains a blocked word: True
The single scan finds every occurrence of every pattern in O(n + matches) time --
independent of the pattern count, unlike running one search per pattern. It powers
virus scanners, spam filters, and DNA motif search.
wrote C:\Users\acwic\symplectic-nbody\examples\output\aho_corasick.svgFloyd-Warshall: shortest paths between every pair
Dijkstra gives shortest paths from one source; for a routing table or a road-network distance matrix you need them between ALL pairs. Floyd-Warshall does it in one elegant O(V^3) dynamic program that also handles negative edge weights (which Dijkstra cannot) and detects negative cycles. It allows ever-larger sets of intermediate nodes: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) as k runs over every node -- three nested loops, no priority queue. Recording the next hop per pair reconstructs the routes; a negative value on the diagonal means a negative cycle; and swapping min for boolean OR gives the transitive closure (who can reach whom). This module computes the distance matrix, paths, cycle detection, and closure, verified against running Dijkstra from every source on 300 random graphs.
Floyd-Warshall: all-pairs shortest paths in one O(V^3) pass
distance matrix (rows = from, cols = to):
0 1 2 3 4 5
0 : 0 7 9 20 20 11
1 : . 0 10 15 21 12
2 : . . 0 11 11 2
3 : . . . 0 6 .
4 : . . . . 0 .
5 : . . . . 9 0
shortest 0 -> 4: [0, 2, 5, 4] (cost 20)
agrees with all-pairs Dijkstra: True
Negative weights are fine (Dijkstra can't do these), and a negative loop is flagged:
0->1->2->0 with weights 1,-3,1 has a negative cycle: True
transitive closure: node 0 can reach [0, 1, 2, 3, 4, 5]
Three nested loops, no priority queue -- it also gives reachability (boolean OR)
and detects negative cycles (a negative diagonal). Used for routing tables and
network distance matrices.
wrote C:\Users\acwic\symplectic-nbody\examples\output\floyd_warshall.svgMisra-Gries: frequent items of a stream in tiny memory
Which items appear more than n/k times in a stream, when a counter per distinct value is impossible for billions of distinct items? The Misra-Gries summary finds every such heavy hitter with only k-1 counters in a single pass. Its rule is a generalized vote: increment a tracked item, start tracking a new one if a slot is free, else decrement every counter (the incoming item cancels one of each). Any item over n/k is guaranteed to survive (no false negatives), and a cheap second pass counts the survivors exactly to drop the false positives; the carried counts underestimate by at most n/k. The special case k=2 is the Boyer-Moore majority vote -- the strict-majority element with one counter. This module builds the summary, verifies candidates, and does majority vote, all checked exhaustively against exact counting.
Misra-Gries: heavy hitters (> n/k) of a 5000-item stream with 3 counters
distinct items: 455 (exact counting would need 455 counters)
Misra-Gries uses only k-1 = 3
item true count approx > n/k?
A 1736 725 yes
B 1240 230 no
C 781 1 no
heavy hitters (verified): {'A': 1736}
matches exact: True
Majority vote (Boyer-Moore, k=2, one counter): winner = A (A has 260/500)
One pass, k-1 counters, no matter how many distinct items stream by: the real
heavy hitters are never missed (a cheap second pass drops the false positives).
Used for network traffic monitors, trending queries, and word-frequency counting.
wrote C:\Users\acwic\symplectic-nbody\examples\output\misra_gries.svgReservoir sampling: a uniform sample from an endless stream
Keep a uniform random sample of k items from a stream whose length you do not know and cannot store -- log lines, sensor readings, a file too big for memory. Vitter's Algorithm R does it in one pass with O(k) memory: fill the reservoir with the first k items, then keep the i-th item with probability k/i, evicting a random existing one. A short induction shows every item ever seen is in the final sample with probability exactly k/n, whatever n turns out to be (k=1 is the classic 'random line from a file'). The weighted Efraimidis-Spirakis variant gives each item a key u^(1/w) and keeps the largest, sampling in proportion to weight. This module implements both plus a streaming reservoir object, and verifies the uniformity with a chi-square test over tens of thousands of runs.
Reservoir sampling: keep a uniform k-sample of a stream of unknown length
one pass, O(k) memory; a 5-sample of a million-item stream: [668303, 839595, 938864, 958802, 959292]
Uniformity check: sample 4 of 12 items, 60000 times.
each element should be picked ~ trials*k/n = 20000 times:
min 19942, max 20062, chi-square 0.78 (11 dof, 1% critical ~ 24.7) -> uniform
Weighted reservoir (Efraimidis-Spirakis): chance tracks weight.
item weight sampled % weight %
rare 1 6.0% 6.2%
common 3 18.8% 18.8%
dominant 12 75.2% 75.0%
Every item ever seen ends up in the sample with probability exactly k/n, no
matter how long the stream. It powers log sampling, A/B test bucketing, and
random line selection from a file too big to hold in memory.
wrote C:\Users\acwic\symplectic-nbody\examples\output\reservoir.svgCount-Min sketch: frequency estimates in sublinear memory
How often has each item appeared, when a counter per distinct key is impossible? The Count-Min sketch estimates every item's count from a fixed d x w grid of counters with d hash functions. Add an item by incrementing one counter per row; query it by taking the MINIMUM of its d counters -- since collisions only inflate a counter, the smallest is the tightest overestimate and the true count is never above it. With width e/epsilon and depth ln(1/delta), the estimate exceeds the truth by more than epsilon * total with probability at most delta, so a few kilobytes track a stream of any size. Counts combine additively, so two sketches merge by element-wise addition -- counting is distributed. This module builds the sketch, queries and merges, and verifies it never underestimates and stays within the error bound across many skewed streams.
Count-Min sketch: 2815 distinct items counted in a 5 x 2719 grid (13595 counters)
item true estimate error
A 5956 5956 0
B 3565 3567 2
C 2040 2040 0
never underestimates: 0 of 2815 items fell below the truth
error bound e/w * total = 20.0; max observed error = 6
Wider tables mean smaller error (fixed depth 4, 20k-item stream):
width counters max error
128 512 87
256 1024 48
512 2048 23
1024 4096 15
2048 8192 9
4096 16384 7
It never undercounts (collisions can only inflate a counter, and the query takes
the min of the d rows), overshoots by a bounded amount, and merges by addition --
so counting is distributed. Powers network flow monitors and n-gram frequency tables.
wrote C:\Users\acwic\symplectic-nbody\examples\output\count_min.svgThe alias method: O(1) sampling from a weighted die
To draw outcome i with probability p_i, the obvious way builds a cumulative distribution and binary-searches a random number at O(log n) per draw. Walker's alias method does it in O(1) per draw after O(n) setup. It chops the distribution into n equal-area columns, each holding at most two outcomes -- a main and an alias -- by repeatedly pairing an under-full outcome with an over-full one until every column has area 1. A draw is one integer roll (pick a column) plus one float flip (main outcome or its alias): two operations, no search, however many outcomes there are. The result is exact -- long-run frequencies equal the weights. This module builds the table by Vose's algorithm and samples from it, verified against the target weights with chi-square tests. The standard for loot tables, particle spawning, and any hot loop sampling one categorical distribution.
Alias method: O(n) setup, then O(1) per weighted draw
outcome weight target sampled
common 12 0.571 0.569
sometimes 5 0.238 0.239
rare 1 0.048 0.048
uncommon 3 0.143 0.144
chi-square vs target over 100000 draws: 0.09 (3 dof, 5% critical ~ 7.8) -> matches
The alias table (each column holds a main outcome + an alias, both equal-area):
column P(main) main alias
0 1.000 common common
1 0.952 sometimes common
2 0.190 rare common
3 0.571 uncommon common
A draw is one integer roll (pick a column) plus one float (main or its alias) --
two operations, no search, however many outcomes there are. It is the standard for
loot tables, particle spawning, and any hot loop sampling the same distribution.
wrote C:\Users\acwic\symplectic-nbody\examples\output\alias_method.svgFisher-Yates: the only correct way to shuffle
Shuffling looks trivial and almost everyone gets it wrong: the naive 'swap each position with a random position anywhere' makes n^n equally likely swap sequences but only n! permutations, and since n^n is not divisible by n! some orderings come up more often. Fisher-Yates fixes it by shrinking the range -- to place position i, swap it with a random position in [i, n), only the unshuffled tail -- so each of the n! permutations results from exactly one choice sequence and every ordering is equally likely. The same sweep gives a partial-shuffle k-sample (uniform without replacement), and restricting swaps to strictly earlier positions (Sattolo) yields a uniform random single cycle. This module implements all of these and demonstrates the bias by enumerating every permutation: Fisher-Yates is flat, the naive shuffle measurably lumpy.
Fisher-Yates: swap each position with a random one in the UNSHUFFLED tail
ABCDEFGH -> EDGBFHCA
Uniformity over all 24 permutations of 4 items, 60000 trials each:
Fisher-Yates: 24/24 permutations seen, chi-square 4.9 -> uniform
naive swap: 24/24 permutations seen, chi-square 1777.2 -> BIASED
(23 dof, 5% critical ~ 35.2; the naive method fails by orders of magnitude)
sample 5 without replacement from 0..19: [7, 9, 18, 16, 15]
Sattolo cyclic shuffle of 0..7: [7, 6, 4, 0, 3, 1, 2, 5] (single 8-cycle: True)
The naive 'swap with any position' makes n^n equally likely swap sequences but
only n! permutations -- and n^n isn't divisible by n!, so some orderings win. Only
the shrinking range makes every permutation exactly equally likely.
wrote C:\Users\acwic\symplectic-nbody\examples\output\fisher_yates.svgBox-Muller: turning uniform randomness into a bell curve
Random generators give uniform values, but almost every simulation -- Brownian motion, Monte-Carlo finance, noise models -- needs Gaussians. The Box-Muller transform converts a pair of uniforms into a pair of independent standard normals: z0 = sqrt(-2 ln u1) cos(2 pi u2), z1 = sqrt(-2 ln u1) sin(2 pi u2). It is exact -- the polar change of variables onto the 2D Gaussian, whose radius has r^2 exponentially distributed and whose angle is uniform. Marsaglia's polar method rejects to the unit disc and reuses its coordinates, skipping the trig. Scaling by sigma and shifting by mu gives any N(mu, sigma^2). This module implements both, and the tests verify the output's mean, variance, skewness (~0), kurtosis (~3), and the 68-95-99.7 rule over large samples.
Box-Muller: two uniforms -> two independent standard normals
sample of 200000 standard normals:
mean = +0.0028 (target 0)
variance = 1.0033 (target 1)
skewness = +0.0028 (target 0)
kurtosis = 2.9973 (target 3)
68-95-99.7 rule:
within 1 sigma: 0.6821 (normal: 0.6827)
within 2 sigma: 0.9546 (normal: 0.9545)
within 3 sigma: 0.9971 (normal: 0.9973)
Marsaglia polar method (no trig): mean -0.0044, var 1.0016, kurtosis 3.034 -- same distribution, faster.
scaled to N(100, 15^2) (e.g. IQ scores): mean 100.0, sd 15.0
The transform is exact: r = sqrt(-2 ln u1) makes r^2 exponential (the Gaussian
radius), theta = 2 pi u2 is the uniform angle, and (r cos, r sin) is a 2D normal.
Every simulation that needs noise -- Brownian motion, Monte-Carlo finance -- starts here.
wrote C:\Users\acwic\symplectic-nbody\examples\output\box_muller.svgRejection sampling: drawing from any density you can evaluate
You can compute a density f(x) but cannot sample it directly -- an unnormalized posterior, a physics distribution, a hand-drawn shape. Rejection sampling turns 'I can evaluate f' into 'I can sample f': draw a candidate from a simpler proposal g you can sample, and accept it with probability f(x)/(M g(x)) where M bounds f <= M g. Geometrically you throw darts uniformly under the envelope M g and keep those below f -- the kept points are distributed exactly as f, even when f is only known up to a constant (which is why it underlies Bayesian computation). The price is efficiency: the acceptance rate is the area ratio 1/M, so a loose envelope or a high dimension wastes darts. This module does box and general rejection sampling, and the tests verify the sampled moments, the acceptance-rate theory, and a histogram chi-square against the target.
Rejection sampling: keep darts thrown under the envelope that land below f(x)
target: a bimodal density on [-4.0, 4.0] (two unequal Gaussian bumps)
drew 100000 samples; acceptance rate 0.307 (theory 0.307)
sample mean -0.0136, variance 2.6092
histogram chi-square vs target (12 bins): 12.73
Acceptance = area under f / area of the box, so a tighter envelope is faster:
envelope M acceptance
1.01 0.307
2.00 0.155
4.00 0.078
It samples ANY density you can evaluate -- even unnormalized ones -- which is why
it underlies Bayesian computation. The cost is the wasted darts: a loose envelope
or a high dimension makes acceptance tiny, which is what MCMC methods work around.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rejection_sampling.svgWelford's algorithm: mean and variance in one stable pass
The textbook variance E[x^2] - E[x]^2 is a numerical disaster: it subtracts two large, nearly-equal numbers, so on offset data (temperatures near 1e6, timestamps, prices) catastrophic cancellation can even return a NEGATIVE variance. Welford's algorithm updates a running mean and the sum of squared deviations as each datum arrives -- delta = x - mean; mean += delta/n; M2 += delta*(x - new_mean) -- never forming those giant intermediates, so it is both online (no need to store the data) and numerically stable. Terriberry's extension carries M3 and M4 for skewness and kurtosis, and two accumulators merge by combining counts, means, and M2 with a correction -- so statistics over shards combine in parallel, exactly. This module provides the accumulator and merge, verified against a two-pass computation and shown staying exact where the naive formula collapses.
Welford: one-pass, online mean and variance, numerically stable
5000 values ~ N(50, 8^2):
mean 49.9322 (two-pass 49.9322)
variance 65.2773 (two-pass 65.2773)
std 8.0794, skewness +0.0116, kurtosis 2.8991
The naive variance E[x^2]-E[x]^2 collapses when the data has a large offset:
offset true var Welford naive
0e+00 2.0 2.0000 2.0000
1e+03 2.0 2.0000 2.0000
1e+06 2.0 2.0000 2.0000
1e+09 2.0 2.0000 0.0000
1e+12 2.0 2.0000 -134217728.0000
Welford never forms the giant sum-of-squares, so it stays exact; the naive formula
subtracts two nearly-equal huge numbers and loses everything to rounding -- even
returning negative variances. Accumulators also merge, so shards combine in parallel.
wrote C:\Users\acwic\symplectic-nbody\examples\output\welford.svgKahan summation: adding floats without losing the small ones
Add a million small numbers naively and the answer drifts: once the total is large, each tiny addend has fewer mantissa bits to land in and its low-order part is rounded away, so the error grows with n. Kahan's compensated summation carries a correction term c for the bits lost on the previous addition -- y = x - c; t = sum + y; c = (t - sum) - y -- so the error stays bounded by a small constant, as if the sum were computed in twice the precision. Neumaier's variant also handles the case where the next addend exceeds the running total (catastrophic cancellation), and pairwise summation gives O(log n) error growth with no correction term. This module implements all of these plus a compensated dot product and running-mean accumulator, verified against Python's exact math.fsum on ill-conditioned inputs.
Kahan summation: carry the low-order bits lost on each addition
summing 0.1 repeatedly (exact answer = n/10):
n naive error Kahan error pairwise
1000 1.41e-14 0.00e+00 2.27e-15
10000 1.59e-13 0.00e+00 1.48e-15
100000 1.88e-12 0.00e+00 1.82e-15
1000000 1.33e-11 0.00e+00 2.33e-15
10000000 1.61e-10 0.00e+00 1.40e-15
Catastrophic cancellation, exact answer = 2.0:
input [1.0, 1e+100, 1.0, -1e+100]
naive = 0.0 (both 1.0s vanished)
Kahan = 0.0
Neumaier = 2.0 <- recovers it
math.fsum = 2.0
The naive error grows like n * machine-epsilon; Kahan keeps a correction term so
it stays bounded, as if the sum were done in double the precision. It matters for
long dot products, running averages, and any accumulation over millions of terms.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kahan.svgHorner's method: evaluating a polynomial the fast, stable way
Evaluating a polynomial by computing each power separately costs ~2n multiplications and sums terms of wildly different sizes. Horner rewrites it as nested multiplication -- p(x) = (...(a_n x + a_{n-1})x + ...)x + a_0 -- evaluating in exactly n mults and n adds with far better rounding. The same sweep IS synthetic division: the intermediate values are the quotient of dividing by (x - r) and the final value is the remainder p(r) (the Remainder Theorem); a second sweep gives p'(r) for free, which makes Horner the engine of Newton's method for polynomial roots. This module evaluates by Horner, does synthetic division and derivatives, and finds real roots by Newton refinement plus deflation -- verified against direct power-sum evaluation and by substituting the roots back.
Horner: p(x) = ((2x - 6)x + 2)x - 1, evaluated in 3 mults instead of 6
p(x) = 2x^3 - 6x^2 + 2x - 1
p(3) = 5 (direct eval agrees: True)
synthetic division by (x - 3): quotient [2, 0, 2], remainder 5 = p(3)
p(3) = 5, p'(3) = 20 (both in two linear sweeps)
roots of x^3 - 6x^2 + 11x - 6 (Newton + deflation): [1.0, 2.0, 3.0]
each found by p(r)/p'(r) steps, then divided out to find the next.
Multiplication count, Horner (n) vs direct powers (~2n):
degree Horner direct
3 3 6
5 5 10
10 10 20
20 20 40
50 50 100
Horner is the standard polynomial evaluator: fewest operations, and summing
left-to-right in one accumulator gives better rounding than adding separate powers.
wrote C:\Users\acwic\symplectic-nbody\examples\output\horner.svgBracketing root-finders: bisection, secant, false position, Brent
Newton is fast but can diverge; when you have a bracket [a, b] where f changes sign, bracketing methods guarantee convergence. Bisection halves the interval each step (foolproof, linear -- one bit per iteration); the secant method fits a line through the last two points (superlinear, order ~1.618, but not guaranteed); false position keeps the secant inside the bracket (safe and faster than bisection); and Brent's method combines bisection's safety with inverse quadratic interpolation's speed, falling back to bisection when the fast step misbehaves -- which is why it is the default root-finder in most numerical libraries. This module implements all four with a shared bracket interface plus a sign-change scanner, verified against roots of polynomials and transcendentals and checked to agree.
Root-finding: four bracketing methods on x^2 - 2 = 0 (root = sqrt 2)
method root iters error
bisection 1.414213562372 41 6.7e-13
false position 1.414213562373 17 2.7e-13
Brent 1.414213562373 26 0.0e+00
secant 1.414213562373 8 2.2e-16
Brent on transcendental equations (no closed form):
cos x = x: x = 0.7390851332 (8 iters)
x = e^-x: x = 0.5671432904 (6 iters)
e^x = 3x+1: x = 1.9038136944 (11 iters)
Locating every root by scanning for sign changes, then refining with Brent:
x^3 - 6x^2 + 11x - 6 -> [1.0, 2.0, 3.0]
Bisection is foolproof but linear (one bit per step); secant is fast but can
fail; Brent takes the fast interpolation step when it is safe and bisects when it
is not -- guaranteed convergence at near-secant speed, the default in most libraries.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rootfind.svgNumerical quadrature: integrating what algebra cannot
Most integrals have no closed form, so you approximate the area under f by sampling it. The trapezoid rule joins samples with lines (error O(h^2)); Simpson fits parabolas (O(h^4), exact for cubics); Romberg applies Richardson extrapolation to a ladder of halved-step trapezoid estimates, cancelling error terms two orders at a time to reach machine precision in a handful of levels; adaptive Simpson subdivides only where the function is hard; and Gauss-Legendre places n nodes optimally to integrate polynomials of degree 2n-1 exactly. This module implements all five and verifies them against integrals with known values (polynomials, exp, trig, the Gaussian bell), confirming the convergence orders -- trapezoid error quarters and Simpson's sixteenths each time the step is halved.
Numerical quadrature: integral of e^x cos x on [0, pi]
exact value = -12.070346316390
method estimate error
trapezoid (n=100) -12.072331874092 2.0e-03
Simpson (n=100) -12.070346055171 2.6e-07
Gauss-Legendre (8p) -12.070346316390 1.2e-14
adaptive Simpson -12.070346316389 3.6e-13
Romberg -12.070346316390 1.8e-15
Convergence order (error as the sample count n doubles):
n trapezoid ratio Simpson ratio
16 7.77e-02 - 3.95e-04 -
32 1.94e-02 4.0x 2.49e-05 15.9x
64 4.85e-03 4.0x 1.56e-06 16.0x
128 1.21e-03 4.0x 9.73e-08 16.0x
256 3.03e-04 4.0x 6.08e-09 16.0x
Trapezoid halves-the-step-quarters-the-error (O(h^2)); Simpson cuts it 16-fold
(O(h^4)); Romberg and Gauss-Legendre reach machine precision in a few evaluations.
It powers everything from physics simulations to option pricing to Bayesian evidence.
wrote C:\Users\acwic\symplectic-nbody\examples\output\quadrature.svgCubic spline interpolation: a smooth curve through every point
A single high-degree polynomial through many points oscillates wildly between them (Runge's phenomenon). A cubic spline instead fits a separate cubic to each interval and stitches them C^2 -- value, slope, AND curvature match at every join -- giving the smoothest interpolant, the shape a flexible draftsman's ruler naturally takes. The construction reduces to solving for the knot second derivatives, a tridiagonal system solved in O(n) by the Thomas algorithm; the natural spline sets zero end curvature, the clamped spline fixes end slopes. This module builds and evaluates the spline and its derivatives, verified to pass through every knot, be C^2, reproduce cubics exactly, and stay near the true Runge curve where a single polynomial explodes to ~1.9. The interpolation behind fonts, animation, and CAD.
Cubic spline: a separate cubic per interval, stitched C^2 smooth
6 knots; spline passes through them all: True
natural end curvatures: M0 = 0.0, Mn = 0.0
value/slope/curvature at interior knot 3: 0.100 / -1.034 / -0.141
Runge's function 1/(1+25x^2), 11 equally spaced knots -- spline vs one polynomial:
x true spline polynomial
0.70 0.0755 0.0747 -0.2262
0.80 0.0588 0.0588 0.0588
0.90 0.0471 0.0476 1.5787
0.95 0.0424 0.0429 1.9236
The single polynomial oscillates to ~1.6 near the edges (Runge's phenomenon)
while the spline hugs the true curve. The spline minimizes total curvature -- the
shape a flexible ruler takes. It's the interpolation behind fonts, animation, and CAD.
wrote C:\Users\acwic\symplectic-nbody\examples\output\spline.svgThe Fast Fourier Transform: O(n log n) instead of O(n^2)
The discrete Fourier transform turns n samples into their n frequency components -- the recipe behind audio and image compression, spectrum analysis, and fast polynomial multiplication. Computed directly it costs O(n^2); the Cooley-Tukey FFT does the same transform in O(n log n) by splitting the samples into even- and odd-indexed halves, transforming each recursively, and combining them with twiddle-factor butterflies -- collapsing a million-sample transform from 10^12 operations to ~2x10^7, ~50,000x faster. The same butterfly runs backwards for the inverse, and the convolution theorem turns an O(n^2) convolution into three FFTs. This module implements the radix-2 FFT, inverse, naive DFT check, and FFT convolution using only built-in complex numbers, verified to match the DFT, round-trip exactly, recover known frequencies, and satisfy Parseval's identity.
FFT: decompose a signal into its frequency components
signal = cos(2pi*4t) + 0.5 cos(2pi*12t), 64 samples
FFT matches the naive DFT: True
round-trip ifft(fft(x)) == x: True
strongest frequency bins (0..n/2):
bin 4 (4 Hz): magnitude 32.00
bin 12 (12 Hz): magnitude 16.00
bin 24 (24 Hz): magnitude 0.00
-> peaks at 4 Hz and 12 Hz, amplitude ratio 2:1, exactly the input.
Convolution via the FFT (multiply spectra): (1+2x)(3+4x+5x^2) =
[3.0, 10.0, 13.0, 10.0] (= 3 + 10x + 13x^2 + 10x^3)
Operation count, FFT (n log2 n) vs naive DFT (n^2):
n FFT DFT speedup
1024 10,240 1,048,576 102x
32768 491,520 1,073,741,824 2,184x
1048576 20,971,520 1,099,511,627,776 52,428x
For a million samples the FFT is ~50,000x faster -- the difference between
instant and infeasible. It is why digital audio, JPEG, MP3, and MRI all exist.
wrote C:\Users\acwic\symplectic-nbody\examples\output\fft.svgGaussian elimination and LU decomposition: solving A x = b
A x = b -- n equations in n unknowns -- is the most-solved problem in computation: circuit analysis, structural mechanics, least squares, the linearized step of every nonlinear solver. Gaussian elimination row-reduces to triangular form and back-substitutes in O(n^3), and done once it factors A = L U into lower- and upper-triangular matrices, after which each new right-hand side is solved in O(n^2) by two triangular sweeps. Partial pivoting swaps in the largest pivot at each step to keep it numerically stable, recorded as a permutation P so P A = L U. The determinant is the product of U's diagonal times the permutation sign, and the inverse comes from solving against each unit column. This module builds the factorization, solves systems, and computes determinants and inverses, verified by residuals and P A = L U over hundreds of random systems.
Gaussian elimination / LU: solve A x = b, factor once, reuse
A = [[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], b = [8, -11, -3]
solution x = [2.0, 3.0, -1.0] residual ||Ax-b|| = 4.4e-16
LU factorization (P A = L U), row permutation [1, 2, 0]:
L (unit lower): [[1.0, 0.0, 0.0], [0.667, 1.0, 0.0], [-0.667, 0.2, 1.0]]
U (upper): [[-3.0, -1.0, 2.0], [0.0, 1.667, 0.667], [0.0, 0.0, 0.2]]
check P A == L U: True
Reusing the factorization for several right-hand sides (each O(n^2)):
b = [8, -11, -3] -> x = [2.0, 3.0, -1.0]
b = [1, 0, 0] -> x = [4.0, -2.0, 5.0]
b = [0, 5, 5] -> x = [10.0, -5.0, 15.0]
determinant = -1 (sign 1 x product of U's diagonal)
inverse row 0 = [4.0, 3.0, -1.0]
A * inverse = identity: True
Factor once in O(n^3), then every new b costs O(n^2): the reason LU beats
re-eliminating from scratch. Partial pivoting (swap in the largest pivot) keeps it
numerically stable -- it underlies circuit analysis, FEM, and every linear solve.
wrote C:\Users\acwic\symplectic-nbody\examples\output\linsolve.svgQR decomposition and least squares: fitting more data than parameters
Any matrix A (m >= n) factors as A = Q R with Q orthonormal (Q^T Q = I) and R upper-triangular. It is the workhorse of overdetermined systems: to fit a model to more data points than parameters, solving the least-squares problem min ||A x - b|| by R x = Q^T b is far more stable than the normal equations A^T A x = A^T b (which square the condition number). The construction is the Gram-Schmidt process -- subtract from each column the components along the earlier orthonormal directions, then normalize -- in its modified form, which subtracts each projection immediately to stay orthogonal under rounding. QR also drives eigenvalue iteration and orthogonal regression. This module builds the thin QR by modified Gram-Schmidt, solves least-squares and square systems, and verifies Q^T Q = I, Q R = A, and that the residual is orthogonal to the column space.
QR decomposition: A = Q R, Q orthonormal, R upper-triangular
Q^T Q = I (orthonormal columns): True
Q R reconstructs A: True
R (upper-triangular): [[14.0, 21.0, -14.0], [0.0, 175.0, -70.0], [0.0, 0.0, 35.0]]
Least-squares line fit to 25 noisy points (true slope 1.7, intercept 3.0):
fitted y = 1.724 x + 3.178
residual ||Ax - b|| = 7.408
Least-squares quadratic fit (exact data): 3.00 x^2 + -2.00 x + 5.00 (true 3, -2, 5)
QR solves least squares via R x = Q^T b -- more stable than the normal equations
A^T A x = A^T b (which squares the condition number). It also powers eigenvalue
iteration and orthogonal regression. Modified Gram-Schmidt keeps Q orthogonal.
wrote C:\Users\acwic\symplectic-nbody\examples\output\qr.svgPower iteration: eigenvalues without the characteristic polynomial
An eigenvector is a direction a matrix only stretches: A v = lambda v. They govern vibration modes, Markov stationary distributions, PCA axes, and PageRank -- and for anything beyond 2x2 the characteristic polynomial is a poor way to find them. Power iteration is the simplest alternative: start from a random vector and repeatedly multiply by A and normalize; each multiply amplifies the largest-|eigenvalue| direction most, so the vector converges to the dominant eigenvector and the Rayleigh quotient v^T A v / v^T v gives its eigenvalue. Shifted inverse iteration power-iterates (A - mu I)^-1 to target the eigenvalue nearest mu, and deflation peels off found eigenpairs to recover a symmetric matrix's whole spectrum. This module implements all three, verified by A v = lambda v, the trace/determinant identities, and analytic cases.
Power iteration: multiply by A and normalize -> dominant eigenvector
dominant eigenvalue = 4.745281 (converged in 30 iterations)
eigenvector = [0.7779, 0.5798, 0.234, 0.0625]
residual ||A v - lambda v|| = 2.51e-06
Shifted inverse iteration targets any eigenvalue (the one nearest the shift):
nearest 0.5: lambda = 0.254719
nearest 2.0: lambda = 1.822717
nearest 3.5: lambda = 3.177283
Full spectrum by deflation: [4.7453, 3.1773, 1.8227, 0.2547]
sum of eigenvalues = 10.0000 (trace = 10) -- they match
No characteristic polynomial, no root-finding: each multiply amplifies the
largest-|lambda| direction, so the vector aligns with it. It is how PageRank ranks
the web, PCA finds data axes, and vibration modes of a structure are computed.
wrote C:\Users\acwic\symplectic-nbody\examples\output\eigen.svgConjugate gradient: huge sparse SPD systems without a factorization
For a symmetric positive-definite A, solving A x = b by LU costs O(n^3) and stores the whole factorization -- impossible at millions of rows (finite-element meshes, image operators, graph Laplacians). Conjugate gradient solves it with nothing but matrix-vector products, so a sparse A costs O(nnz) per step and O(n) memory. It minimizes the energy (1/2)x^T A x - b^T x, choosing each search direction A-conjugate to all previous ones so it never undoes earlier progress -- converging in at most n steps exactly, far fewer in practice at a rate set by sqrt(kappa), which is why preconditioning (here the Jacobi diagonal) is the whole game. This module implements CG and preconditioned CG, verified against a dense LU solve, the <= n step guarantee, and the monotone residual decay, and shown beating steepest descent's zig-zag.
Conjugate gradient: minimize (1/2)x^T A x - b^T x with mat-vecs only
20x20 SPD system:
CG converged in 12 iterations (<= n = 20)
residual ||A x - b|| = 1.96e-09
matches LU: True
Steepest descent on the same system: 22 iterations to the same tol
-- CG picks A-conjugate directions so it never undoes earlier progress;
steepest descent zig-zags: 1.8x as many steps.
Jacobi-preconditioned CG: 12 iterations (residual 2.7e-09)
No factorization, O(nnz) per step, O(n) memory: CG (and its preconditioned
cousins) solve the million-unknown SPD systems of finite elements, image
reconstruction, and graph Laplacians that a dense LU could never store.
wrote C:\Users\acwic\symplectic-nbody\examples\output\conjugate_gradient.svgSVD and PCA: the axes a matrix acts along
Every matrix A factors as A = U S V^T: orthonormal input directions V, orthonormal outputs U, and non-negative singular values S saying how much A stretches each -- geometrically, A sends the unit sphere to an ellipsoid whose semi-axes are the singular values. It is the most informative factorization: the rank, the 2-norm and condition number, the best low-rank approximation (Eckart-Young, the basis of image compression), and the pseudo-inverse all read off it. Here it is built via the symmetric eigendecomposition of A^T A (reusing the power-iteration eigensolver). Principal component analysis is SVD of mean-centred data: the top singular vectors are the directions of greatest variance. This module computes the thin SVD, low-rank reconstruction, and PCA with explained variance, verified by A = U S V^T, orthonormality, and the variance ordering.
SVD: A = U S V^T -- orthonormal axes and the stretches between them
singular values: [6.491, 3.4449]
reconstruction A = U S V^T holds: True
rank 2, spectral norm 6.4910, condition number 1.8842
Low-rank approximation (Eckart-Young) of a 6x6 nearly-rank-1 matrix:
rank k approximation error
1 0.0841
2 0.0617
3 0.0449
6 0.0000
-- rank 1 already captures almost everything: the basis of image/data compression.
PCA of a tilted 2D cloud (120 points):
component 1 [0.881, 0.474], explains 96.1% of variance
component 2 [-0.474, 0.881], explains 3.9%
SVD gives the rank, the 2-norm, the condition number, the best low-rank fit, and
the pseudo-inverse -- and PCA (SVD of centred data) finds the directions of greatest
variance. It underlies image compression, recommender systems, and latent semantics.
wrote C:\Users\acwic\symplectic-nbody\examples\output\svd.svgk-means clustering: finding groups in unlabelled data
Given points and a number k, k-means partitions them so each belongs to the nearest centroid, minimizing the total within-cluster squared distance (the inertia). Lloyd's algorithm alternates two steps -- assign each point to its nearest centroid, then move each centroid to its members' mean -- each of which can only lower the inertia, so it converges (to a local minimum). k-means++ seeding spreads the initial centres by picking each with probability proportional to its squared distance from the nearest chosen one, avoiding bad starts, and a few restarts keep the best. The inertia elbow and the silhouette score both flag the natural number of clusters. This module implements Lloyd's with random and k-means++ init, multi-restart selection, and the silhouette, verified to recover well-separated blobs with monotone inertia.
k-means: partition points so each is nearest its cluster's centroid
160 points, k=4: converged in 3 iterations
cluster sizes [40, 40, 40, 40], inertia 102.3, silhouette 0.851
centroids: [[11.9, 10.0], [2.0, 2.0], [9.0, 3.0], [4.9, 9.1]]
Inertia falls every Lloyd iteration (assign, then re-centre):
after 1 iter(s): inertia 227.2
after 2 iter(s): inertia 102.3 (-124.9)
after 3 iter(s): inertia 102.3 (-0.0)
after 4 iter(s): inertia 102.3 (-0.0)
after 5 iter(s): inertia 102.3 (-0.0)
after 6 iter(s): inertia 102.3 (-0.0)
The 'elbow': inertia vs k drops sharply until the true cluster count, then flattens.
k inertia silhouette
1 4438.1 0.000
2 2118.0 0.523
3 1105.1 0.652
4 102.3 0.851
5 89.9 0.729
6 80.4 0.597
7 69.9 0.483
The elbow at k=4 and the peak silhouette both flag the true four groups. k-means
powers customer segmentation, color quantization, and vector quantization -- and
k-means++ seeding spreads the initial centres to avoid Lloyd's bad local minima.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kmeans.svgLinear and logistic regression: fitting a line and a decision boundary
The two workhorses of supervised learning. Linear regression fits y = w.x + b by minimizing squared error -- solved in closed form by QR least squares (avoiding the normal equations' condition-number squaring) or by gradient descent for large data. Logistic regression predicts a probability sigmoid(w.x + b) for binary labels, fit by gradient descent on the convex log-loss, and its decision boundary w.x + b = 0 is a separating hyperplane. Both are linear models; logistic just squashes through the sigmoid to stay a probability, and an L2 penalty shrinks the weights for generalization. This module fits linear regression by both QR and gradient descent, logistic by gradient descent with optional L2, and reports R^2 for regression and accuracy / log-loss for classification, verified against exact fits and separable data.
Linear regression: least-squares line through noisy data
QR fit: y = 1.442 x + 2.452 (true slope 1.5, intercept 2.0)
GD fit: y = 1.442 x + 2.452 (matches QR)
R^2 = 0.7831
Logistic regression: probability of class 1 vs x (boundary where p = 0.5)
accuracy 100.0%, log-loss 0.0314
decision boundary at x = 5.23 (data splits at 5)
Log-loss falls as gradient descent proceeds:
10 epochs: log-loss 0.5806
100 epochs: log-loss 0.1399
1000 epochs: log-loss 0.0638
6000 epochs: log-loss 0.0314
Linear regression fits a line by minimizing squared error (closed form or GD);
logistic squashes w.x+b through the sigmoid to a probability and minimizes log-loss,
a convex objective with one global minimum. Both are the base of modern ML pipelines.
wrote C:\Users\acwic\symplectic-nbody\examples\output\regression.svgDecision trees: classification by the best yes/no questions
The most interpretable model, and the base learner of random forests and gradient boosting -- the workhorses of tabular ML. CART grows a tree greedily: at each node it tries every feature and every threshold and keeps the split that most reduces the IMPURITY of the children, where Gini impurity is 1 - sum p^2 (the chance two random draws differ) and entropy is -sum p log2 p (bits of surprise), both zero for a pure node. Recursing carves the plane into axis-aligned rectangles, each a leaf that votes the majority class; a max-depth or minimum-node-size cap fights the overfitting a fully grown tree invites. The path from root to leaf reads as a plain if/else rule, and no feature scaling is needed. This module builds the classifier with Gini or entropy, exposes the learned rules and feature importances, and is checked on separable blobs, a train/test split, and a known single split.
Decision tree (CART): recursive best-split classification
90 points, 3 classes, 2 features (x, y)
training accuracy 100.0%, depth 2, 3 leaves
Learned rules (Gini impurity):
if x[0] <= 5.11:
if x[1] <= 4.95:
predict 0 {0: 30}
else:
predict 2 {2: 30}
else:
predict 1 {1: 30}
Feature importances: x = 0.50, y = 0.50
Depth cap trades fit for simplicity:
max_depth=1: 2 leaves, accuracy 66.7%
max_depth=2: 3 leaves, accuracy 100.0%
max_depth=3: 3 leaves, accuracy 100.0%
unlimited: 3 leaves, accuracy 100.0%
Entropy criterion: accuracy 100.0%, 3 leaves
(a pure node has gini 0 and entropy 0; a 50/50 node has gini 0.50, entropy 1.00)
A tree asks the single threshold question that most purifies its samples,
then recurses. The result is a set of axis-aligned rectangles tiling the plane,
each labelled by majority vote -- readable rules, the base learner of forests.
wrote C:\Users\acwic\symplectic-nbody\examples\output\decision_tree.svgRandom forests: a committee of decorrelated trees
A single decision tree overfits -- it memorizes noise by growing pure leaves. A random forest averages many trees deliberately weakened to disagree, so their errors cancel while their signal adds. Two randomizations decorrelate them: BAGGING trains each tree on a bootstrap sample (n rows drawn with replacement, ~63% distinct), and FEATURE SUBSAMPLING lets each split consider only a random sqrt(d) subset of features so no one strong feature dominates every tree. Prediction is a majority vote. Because ~37% of rows are out-of-bag for each tree -- never seen by it -- voting each row over only its out-of-bag trees gives a free, honest validation estimate needing no held-out set. This module builds a bagged forest of the CART learner with per-node feature sampling, majority-vote prediction, out-of-bag scoring, and averaged feature importances, verified to beat an overfit single tree on a noisy problem with its OOB estimate tracking true test error.
Random forest: a committee of decorrelated decision trees
350 train / 150 test points, 2 real + 3 noise features, 10% label flips
single tree (unlimited depth): train 100.0%, test 70.0%, 64 leaves (memorized noise)
random forest (41 trees): train 95.4%, test 84.7%, out-of-bag 81.4%
Out-of-bag error is a free validation estimate -- each row is scored only by the
~37% of trees that never trained on it. Here OOB tracks true test error closely.
Feature importances (real features 0,1 should dominate the 3 noise features):
x: 0.30 ############
y: 0.34 ##############
noise1: 0.11 ####
noise2: 0.11 #####
noise3: 0.13 #####
More trees only help -- test accuracy stabilizes as the ensemble grows:
1 trees: test 72.7%, oob 78.0%
3 trees: test 80.0%, oob 71.1%
9 trees: test 83.3%, oob 74.6%
21 trees: test 84.7%, oob 78.0%
41 trees: test 84.7%, oob 81.4%
Bagging (bootstrap rows) plus per-split feature subsampling decorrelate the trees
so their errors cancel while signal adds. The forest boundary is smoother and more
robust than any single tree -- the strong default for tabular data.
wrote C:\Users\acwic\symplectic-nbody\examples\output\random_forest.svgGaussian mixtures & EM: soft, probabilistic clustering
k-means assigns each point hard to its nearest centroid; a Gaussian mixture instead models the data as drawn from k Gaussians and asks, for each point, the PROBABILITY it came from each -- a soft assignment that lets clusters differ in size, weight, and spread, and that comes with a likelihood. The fit is Expectation-Maximization: the E-step computes each point's responsibility (posterior over components) with the parameters fixed, and the M-step re-estimates each component as the responsibility-weighted mean, variance, and weight of the data. Each round provably cannot decrease the log-likelihood -- that monotone climb is the standard correctness check -- and EM converges to a local optimum, so it is run from several inits and the best kept. This module fits a diagonal-covariance mixture in any dimension with log-sum-exp numerics, gives soft responsibilities and hard labels, and reports AIC/BIC for choosing k -- verified to recover known mixture parameters, climb the log-likelihood every iteration, and let BIC select the true number of components.
Gaussian mixture model (EM): soft, probabilistic clustering
360 points, 3 clusters of unequal spread, 2-D
converged in 25 iterations, log-likelihood -1259.0
Recovered components (true means (2,2) sd .5, (8,8) sd 1.4, (2,8) sd .9):
mean ( 1.89, 8.04) sd (0.80,0.80) weight 0.33
mean ( 2.06, 2.01) sd (0.48,0.46) weight 0.33
mean ( 7.81, 7.90) sd (1.53,1.24) weight 0.34
EM climbs the log-likelihood monotonically (first / last 4 iterations):
iter 0: -2065.9
iter 1: -1642.4
iter 2: -1514.4
iter 3: -1496.8
iter 21: -1259.0
iter 22: -1259.0
iter 23: -1259.0
iter 24: -1259.0
BIC selects the number of components (lower is better):
k=1: BIC 3622.7 AIC 3607.2
k=2: BIC 2775.7 AIC 2740.7
k=3: BIC 2600.4 AIC 2546.0
k=4: BIC 2613.4 AIC 2539.6
k=5: BIC 2623.0 AIC 2529.8
BIC minimized at k=3 (true is 3)
Unlike k-means' hard nearest-centroid assignment, a mixture gives each point a
probability of belonging to each component, so clusters can differ in size, weight,
and spread -- and the likelihood lets BIC/AIC choose k in a principled way.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gmm.svgHidden Markov models: decoding sequences with hidden state
A hidden Markov model describes a sequence you can see (emissions) generated by a chain of states you cannot (the hidden path) -- the weather driving whether you carry an umbrella, or a dealer switching between a fair and a loaded die. Three questions have three exact dynamic-programming answers over the trellis: the FORWARD algorithm sums the probability of every consistent path in O(T k^2) to evaluate the sequence (a naive sum is O(k^T)); VITERBI is the same recursion with max for sum plus backpointers, giving the single most-likely hidden path; and BAUM-WELCH is EM -- forward-backward gives the posterior of each state and transition at each step, and those soft counts re-estimate the matrices, the likelihood provably climbing each round. It all runs in log space with log-sum-exp so long sequences never underflow. This module implements forward, Viterbi, forward-backward posteriors, and Baum-Welch training, verified that Viterbi recovers a planted path, forward and backward agree on the likelihood, and Baum-Welch relearns a known loaded-die model with monotone log-likelihood.
Hidden Markov model: the occasionally-dishonest casino
260 die rolls; hidden state is fair (0) or loaded (1)
forward log-likelihood of the rolls: -442.96
Viterbi decode accuracy vs the true hidden path: 76.9%
fraction of time loaded -- true 0.31, Viterbi 0.13
forward-backward posterior: 24 rolls flagged loaded with >90% confidence
Baum-Welch relearns the model from the rolls alone (no hidden states given):
learned P(roll a six | loaded state) = 0.67 (true 0.60)
learned P(roll a six | fair state) = 0.20 (true 0.17)
learned P(stay loaded) = 0.76 (true 0.88)
trained log-likelihood -434.83 vs true-model -442.96
Three exact dynamic-programming algorithms over the trellis: forward sums all paths
(evaluate), Viterbi maxes over them (decode), Baum-Welch uses forward-backward soft
counts to relearn the matrices (EM). All in log space so long sequences never underflow.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hmm.svgThe Kalman filter: optimal tracking of a hidden state
Where a hidden Markov model tracks a DISCRETE hidden state, the Kalman filter tracks a CONTINUOUS one -- a position and velocity, a trajectory -- from noisy measurements in real time, with no growing history. It is the optimal estimator for a linear system with Gaussian noise, and the math behind GPS, guidance, and sensor fusion. The world is a linear-Gaussian state space: the true state evolves as x <- F x + process noise, the sensor reports z = H x + measurement noise. The filter carries a Gaussian belief (mean, covariance) and alternates PREDICT (push through the dynamics, uncertainty grows) and UPDATE (fold in a measurement weighted by the Kalman gain K = P H' (H P H' + R)^-1, uncertainty shrinks). Because both model and sensor are noisy, the fused estimate beats either alone -- its variance provably below the sensor's -- and a backward RTS smoother, using future data, beats the causal filter. This module implements the multivariate filter and smoother with self-contained matrix helpers, verified on constant-velocity tracking: error and variance fall below the raw measurements', a steady-state gain is reached, a perfect sensor is trusted exactly and a useless one ignored, and the smoother improves on the filter.
Kalman filter: optimal tracking of a hidden state from noisy measurements
80 steps, position sensor with noise sd = 6.0
RMSE vs truth: raw measurements 6.40
Kalman filter 3.52 (45% better than raw)
RTS smoother 2.18 (66% better than raw)
Estimate variance collapses from 24.0 to a steady 8.60 (measurement variance is 36):
step 0: var 24.00 ##############################
step 1: var 24.01 ##############################
step 2: var 22.51 ############################
step 5: var 16.39 ####################
step 10: var 11.05 ##############
step 40: var 8.60 ###########
step 79: var 8.60 ###########
Velocity is never measured, only inferred: final estimate 3.64 (truth ~3.60).
Predict pushes the belief through the motion model (variance grows); update folds in
a measurement weighted by the Kalman gain (variance shrinks). The fused estimate beats
either the model or the sensor alone -- and the backward smoother, using future data,
beats the causal filter. This is the math behind GPS, guidance, and sensor fusion.
wrote C:\Users\acwic\symplectic-nbody\examples\output\kalman.svgPageRank: ranking a graph by its random walk
The algorithm that launched Google, and a clean application of Markov chains and the dominant eigenvector. PageRank scores every node of a directed graph by one recursive idea -- a node is important if important nodes link to it -- formalized as a random surfer who with probability d follows a random out-link and with probability 1-d teleports to a uniformly random page. The score is the fraction of time the surfer spends on each page: the STATIONARY DISTRIBUTION of that chain, equivalently the dominant eigenvector of the Google matrix G = d M + (1-d)/N 11'. The teleport makes G strictly positive, so Perron-Frobenius guarantees a unique positive stationary vector and POWER ITERATION converges to it geometrically at rate d. Dangling nodes (no out-links) would leak probability, so their mass is redistributed by teleport, and it is all done sparsely without forming the dense NxN matrix. This module computes PageRank by sparse power iteration with damping and correct dangling handling, plus the personalized variant, verified against the analytic stationary distribution of small chains, ring symmetry, and the fixed-point property.
PageRank: ranking a small web by the random surfer's stationary distribution
8 pages, 13 links, damping 0.85
Rank (share of surfer's time), highest first:
home: 0.3313 in-links 5 ########################################
blog: 0.2137 in-links 3 ##########################
shop: 0.1463 in-links 2 ##################
about: 0.1126 in-links 1 ##############
post1: 0.0793 in-links 1 ##########
post2: 0.0793 in-links 1 ##########
ad: 0.0188 in-links 0 ##
orphan: 0.0188 in-links 0 ##
Scores sum to 1.000000 (a probability distribution).
Note 'home' wins not just on in-link COUNT but because important pages link to it --
that recursive definition is the whole point; raw in-degree would tie several pages.
Personalized PageRank (teleport home = 'shop') re-weights the graph:
shop: uniform 0.1463 -> shop-personalized 0.2736
home: uniform 0.3313 -> shop-personalized 0.3672
blog: uniform 0.2137 -> shop-personalized 0.1629
Power iteration converges geometrically (rate ~ damping = 0.85):
step 0: L1 change 8.85e-01
step 1: L1 change 4.62e-01
step 2: L1 change 3.07e-01
step 4: L1 change 2.22e-01
step 8: L1 change 1.16e-01
step 16: L1 change 3.16e-02
The teleport term makes the Google matrix strictly positive, so Perron-Frobenius
guarantees a unique stationary vector and power iteration converges at rate d.
wrote C:\Users\acwic\symplectic-nbody\examples\output\pagerank.svgLU & Cholesky: factoring a matrix to solve, invert, and take determinants
Solving A x = b once is easy; solving it for many right-hand sides, or getting det(A) or A^-1, is where FACTORIZATION pays. LU decomposition writes any square matrix as P A = L U -- a permutation of row swaps (partial pivoting, for stability), a unit-lower-triangular L, and an upper-triangular U -- by Gaussian elimination in O(n^3) once; afterwards each solve is two O(n^2) triangular sweeps, the determinant is the signed product of U's diagonal, and the inverse is n solves. For a symmetric positive-definite matrix, CHOLESKY A = L L' is the smaller, stabler special case: half the work, no pivoting, and it succeeds if and only if the matrix is positive definite -- so attempting it IS the standard SPD test, the backbone of least squares and Kalman filters. This module implements LU with partial pivoting, Cholesky, triangular and general solves, determinant, and inverse, verified by reconstructing P A = L U and A = L L', cross-checking determinants against cofactors, confirming Cholesky rejects non-positive-definite matrices, and round-tripping A A^-1 = I.
LU decomposition: P A = L U by Gaussian elimination with partial pivoting
A =
[ 2.00 1.00 1.00]
[ 4.00 -6.00 0.00]
[ -2.00 7.00 2.00]
L (unit lower triangular) =
[ 1.00 0.00 0.00]
[ 0.50 1.00 0.00]
[ -0.50 1.00 1.00]
U (upper triangular) =
[ 4.00 -6.00 0.00]
[ 0.00 4.00 1.00]
[ 0.00 0.00 1.00]
pivot row order [1, 0, 2], permutation sign -1
reconstruction max|PA - LU| = 0.00e+00
det(A) = sign * prod(diag U) = -16.0 (== determinant() -16.0)
Factor once, then solve for many right-hand sides cheaply (O(n^2) each):
A x = [5.0, -2.0, 9.0] -> x = [1.000, 1.000, 2.000] (A x back = [5.0, -2.0, 9.0])
A x = [1.0, 0.0, 0.0] -> x = [0.750, 0.500, -1.000] (A x back = [1.0, 0.0, 0.0])
A x = [0.0, 1.0, 1.0] -> x = [-0.688, -0.625, 2.000] (A x back = [0.0, 1.0, 1.0])
Cholesky: A = L L' for a symmetric positive-definite A (half the work, no pivoting)
A =
[ 4.00 2.00 2.00]
[ 2.00 5.00 3.00]
[ 2.00 3.00 6.00]
L =
[ 2.00 0.00 0.00]
[ 1.00 2.00 0.00]
[ 1.00 1.00 2.00]
reconstruction max|L L' - A| = 0.00e+00
Attempting Cholesky IS the positive-definiteness test:
SPD [[4,2,2],...]: positive definite = True
indefinite [[1,2],[2,1]]: positive definite = False
negative [[-1,0],[0,1]]: positive definite = False
LU factors any square matrix and powers solves, determinants, and inverses;
Cholesky is the smaller, stabler SPD special case behind least squares and Kalman.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lu.svgGaussian process regression: prediction with honest error bars
Where linear regression fits fixed coefficients, a Gaussian process fits a distribution over FUNCTIONS and returns a calibrated error bar that widens where there is no data. It assumes any finite set of function values is jointly Gaussian with covariance set by a KERNEL -- the RBF kernel k(x,x') = sigma^2 exp(-||x-x'||^2/2l^2) encoding 'smooth, with length scale l'. Conditioning that joint Gaussian on the observations gives the posterior in closed form: mean = k*'(K+sigma_n^2 I)^-1 y and variance = k(x*,x*) - k*'(K+sigma_n^2 I)^-1 k*, the single linear solve done by a Cholesky factorization of the SPD matrix (K + noise) and reused for the log MARGINAL LIKELIHOOD that scores hyperparameters. Noise-free, the posterior interpolates the data exactly with zero variance there; far from data the variance rises back to the prior. This module builds GP regression with the RBF kernel, posterior mean and variance, and marginal-likelihood length-scale selection -- verified to interpolate noise-free data exactly, grow uncertainty away from data, recover a known smooth function, and peak the marginal likelihood near the true length scale.
Gaussian process regression: prediction with honest error bars
8 noisy observations of a smooth function, with a gap in the middle
length scale chosen by max marginal likelihood: l = 0.7 (log ML -7.12)
marginal likelihood across the grid:
l = 0.2: log ML -9.98
l = 0.35: log ML -9.37
l = 0.5: log ML -8.38
l = 0.7: log ML -7.12 <- selected
l = 1.0: log ML -7.48
l = 1.4: log ML -12.20
l = 2.0: log ML -22.94
l = 3.0: log ML -50.10
Posterior 2-sigma band: ~0.16 at the data, ~1.99 in the middle gap -- uncertainty tracks where data is.
100% of the true curve lies inside the 2-sigma band (well-calibrated).
A GP places a prior over smooth functions via a kernel, then conditions on the data
in closed form -- one Cholesky solve gives both the mean and the variance. The band
pinches to zero at noise-free data and flares back to the prior far away: uncertainty
you can actually trust, which is why GPs drive Bayesian optimization and active learning.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gaussian_process.svgBayesian optimization: minimizing an expensive black box
Some objectives are costly to evaluate -- a hyperparameter sweep that trains a model each time, a physical experiment, a slow simulation -- and grid or random search wastes most of the budget on uninteresting regions. Bayesian optimization builds a cheap probabilistic surrogate (a Gaussian process) of the objective and spends each expensive evaluation where an ACQUISITION FUNCTION says the expected payoff is highest, balancing EXPLOITATION (sample where the surrogate predicts a low value) against EXPLORATION (sample where it is uncertain). The classic acquisition is EXPECTED IMPROVEMENT: with current best f_best and posterior (mu, sigma), EI = (f_best - mu) Phi(z) + sigma phi(z) where z = (f_best - mu)/sigma -- zero at observed points, large where the surrogate is both promising and unsure, so maximizing it trades the two off automatically. This module implements EI and the full loop over a bounded domain, verified to locate the minima of a 1-D multimodal function (to within 0.001 of optimum in 20 evaluations) and the 2-D Branin function, and to beat random search at equal budget.
Bayesian optimization: minimize an expensive black box in few evaluations
1-D multimodal target on [2.7, 7.5], 20 total evaluations
found minimum at x = 5.136, f = -1.8990
true minimum at x = 5.146, f = -1.8996
gap to optimum: 0.0006
Best-so-far value as evaluations accrue:
after 1 evals: -0.4240
after 4 evals: -1.8958
after 6 evals: -1.8958
after 9 evals: -1.8970
after 13 evals: -1.8983
after 20 evals: -1.8990
On 2-D Branin (global min 0.398), BO vs random search at equal budget:
Bayesian opt : mean 0.446, best 0.407 (all runs within 0.5 of optimum)
random search: mean 3.339, best 0.826 (erratic)
The GP surrogate turns a few expensive samples into a full belief over the objective;
Expected Improvement then spends each new evaluation where the payoff is highest,
balancing exploiting the current best against exploring the uncertain regions. This is
how modern hyperparameter tuners and experiment optimizers work.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bayes_opt.svgDBSCAN: density clustering of arbitrary shapes
k-means and Gaussian mixtures need you to pick k and assume blobby clusters. DBSCAN assumes neither: it finds clusters as connected regions of high point density, discovers their number automatically, handles arbitrary shapes (interlocking moons, concentric rings), and explicitly labels outliers as NOISE instead of forcing every point into a cluster. Two parameters set 'dense enough' -- a radius EPS and a count MIN_PTS: a CORE point has at least min_pts neighbours within eps, a BORDER point is within eps of a core but not itself core, and everything else is NOISE. A cluster grows by starting at a core point and flood-filling through core-to-core neighbourhoods, so an S-curve is recovered whole where k-means would slice it. This module implements DBSCAN with the core/border/noise classification and a k-distance helper for choosing eps, verified to separate two interlocking half-moons that k-means cannot, flag sparse outliers as noise, discover the cluster count on its own, and degenerate sensibly at extreme parameters.
DBSCAN: density-based clustering of arbitrary shapes with noise
232 points (220 on two interlocking moons + 12 outliers), eps = 0.22, min_pts = 5
clusters discovered (no k given): 2
points flagged as noise: 4
Point classification:
core: 224
border: 4
noise: 4
Cluster sizes:
cluster 0: 115 points
cluster 1: 113 points
The k-distance graph (sorted distance to the 4th neighbour) -- its elbow marks eps:
10th pct: 0.056 ##
30th pct: 0.072 ###
50th pct: 0.084 ###
70th pct: 0.103 ####
90th pct: 0.140 ######
97th pct: 0.234 #########
Because it grows clusters by local density instead of distance to a centroid, DBSCAN
recovers the crescents as single clusters (k-means would cut each in half), needs no k,
and calls sparse points noise instead of forcing them into a cluster.
wrote C:\Users\acwic\symplectic-nbody\examples\output\dbscan.svgHierarchical clustering: a tree of nested groupings
k-means and DBSCAN give one flat partition; hierarchical clustering gives the whole family at once, as a tree. Agglomerative clustering starts with every point its own cluster and repeatedly merges the two closest, recording each merge and the distance at which it happened -- the DENDROGRAM. Cut it at any height to read off a flat clustering, so the number of clusters comes from where you cut, often the biggest gap in merge heights, rather than being fixed in advance. What 'closest' means is the LINKAGE: SINGLE (nearest points, tends to chain along filaments), COMPLETE (farthest points, compact clusters), AVERAGE (mean pairwise, UPGMA), and WARD (least increase in within-cluster variance, tight and spherical). Merge heights are monotone for these, so the tree has no crossings. This module builds the full dendrogram by the Lance-Williams update and cuts it into k clusters, verified to recover well-separated blobs with all linkages, climb merge heights monotonically, and show single-linkage chaining where complete linkage stays compact.
Hierarchical agglomerative clustering: a tree of nested groupings
24 points, Ward linkage, 23 merges to one cluster
Merge heights climb monotonically (no dendrogram inversions):
merge 0: height 0.113
merge 1: height 0.181
merge 2: height 0.210
merge 11: height 0.598
merge 21: height 16.961
merge 22: height 18.230
monotone: True
Largest jump in merge height is before the final 3 merges -> suggests 3 clusters.
Cutting into 3 clusters gives sizes {0: 8, 1: 8, 2: 8}
clusters recover the true blobs: True
Linkage changes cluster shape -- on a long chain of 24 evenly-spaced points, cut in 2:
single: split sizes [5, 19] chains (peels an end)
complete: split sizes [10, 14] compact (even split)
average: split sizes [10, 14]
ward: split sizes [10, 14]
Every merge is recorded with the distance at which it happened, so one run yields
the entire family of clusterings: cut the tree low for many tight groups, high for a
few broad ones. No k needed up front -- read it off the biggest gap in merge heights.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hierarchical.svgNaive Bayes: fast probabilistic classification
Naive Bayes applies Bayes' theorem with one bold simplification -- it assumes the features are conditionally independent given the class. That is 'naive' (words and pixels are not independent), but it collapses a joint distribution into a product of per-feature terms, so training is a single counting pass and prediction a sum of logs. The posterior is P(c | x) proportional to P(c) prod_i P(x_i | c); in log space we pick the class maximizing log P(c) + sum_i log P(x_i | c). Two flavours differ only in P(x_i | c): GAUSSIAN models each continuous feature as a per-class Normal (estimate mean and variance), while MULTINOMIAL models word counts with Laplace add-alpha smoothing so an unseen word never zeroes the product -- the workhorse of spam filters. This module implements both entirely in log space, verified that the Gaussian model separates blobs and matches a hand-computed posterior exactly, that the multinomial model classifies documents and its smoothing prevents zero probabilities, and that the predicted class-probabilities are normalized. Despite the crude assumption it is the strong baseline every fancier classifier must beat.
Gaussian naive Bayes: continuous features, one Gaussian per class per feature
105 points, 3 classes, training accuracy 100.0%
Learned per-class feature statistics:
class 0: mean (-0.16, 0.10) sd (0.52,0.52) prior 0.33
class 1: mean ( 6.06, 1.07) sd (0.87,0.97) prior 0.33
class 2: mean ( 2.10, 6.05) sd (0.47,0.46) prior 0.33
Posterior class probabilities at a few points:
[0.0, 0.0]: c0=1.00, c1=0.00, c2=0.00
[3.0, 3.0]: c0=0.00, c1=1.00, c2=0.00
[6.0, 1.0]: c0=0.00, c1=1.00, c2=0.00
Multinomial naive Bayes: word counts, a classic spam filter
8 documents, vocab ['free', 'money', 'offer', 'meeting', 'report', 'project'], training accuracy 100.0%
Per-word evidence, log P(word|spam) - log P(word|ham) (positive => spammy):
free: +2.54 +++++++++++++++
money: +1.48 ++++++++
offer: +1.48 ++++++++
meeting: -2.51 ---------------
report: -1.53 ---------
project: -1.53 ---------
Classifying new documents:
[free, money, offer] -> spam (P(spam)=1.00)
[meeting, report, project] -> ham (P(spam)=0.00)
[free, meeting, report] -> ham (P(spam)=0.18)
The independence assumption is false -- words co-occur -- yet summing per-feature
log-evidence gives a fast, strong classifier trained in a single counting pass. It is
the baseline every fancier text or tabular model has to beat.
wrote C:\Users\acwic\symplectic-nbody\examples\output\naive_bayes.svgk-nearest-neighbours: lazy, instance-based learning
k-NN is the ultimate lazy learner: it builds no model. To predict a point it finds the k nearest training points and lets them VOTE (classification) or AVERAGES their values (regression) -- all the work at query time, a non-parametric decision boundary that traces the data. The number of neighbours k trades variance for bias: k=1 fits every point exactly (a jagged, noise-fitting boundary) while large k averages over a wide region (smooth but blurred), and the vote can be uniform or DISTANCE-WEIGHTED (weight = 1/distance, so closer neighbours count more). Because it compares raw coordinates it is sensitive to feature scaling, so standardization is included. A brute-force search is O(n) per query; a k-d tree cuts that to O(log n) in low dimensions. This module implements k-NN classification and regression with both weightings and leave-one-out cross-validation, verified that 1-NN memorizes the training labels, that it recovers separable classes and a smooth regression target, that distance-weighting follows the closest neighbour, that LOO selects k>1 under label noise, and that its neighbours agree with the k-d tree.
k-nearest-neighbours: lazy, non-parametric classification
120 points, 2 classes split by a diagonal with 15% label noise
Training accuracy vs k (k=1 memorizes, larger k smooths):
k = 1: train accuracy 100.0%
k = 3: train accuracy 89.2%
k = 7: train accuracy 88.3%
k = 15: train accuracy 88.3%
k = 31: train accuracy 88.3%
Leave-one-out cross-validation (honest accuracy) picks k:
k = 1: LOO 0.750 ##############################
k = 3: LOO 0.825 #################################
k = 5: LOO 0.825 #################################
k = 7: LOO 0.850 ##################################
k = 9: LOO 0.867 ###################################
k = 11: LOO 0.850 ##################################
k = 15: LOO 0.875 ################################### <- best
k = 21: LOO 0.875 ###################################
k = 31: LOO 0.858 ##################################
Best k by LOO = 15: k=1 overfits the noise, very large k oversmooths.
k-NN regression on a noisy sine (k=5): R^2 uniform 0.952, distance-weighted 1.000
k-NN builds no model at all -- it just stores the data and votes among the nearest
neighbours at query time. Small k means low bias but high variance (jagged, noise-
fitting); large k means the opposite. Cross-validation finds the balance.
wrote C:\Users\acwic\symplectic-nbody\examples\output\knn.svgGradient boosting: shallow trees that correct each other
A random forest averages independent deep trees; gradient boosting grows trees in SEQUENCE, each correcting the errors of those before it. It is gradient descent in FUNCTION space: start with a constant, then repeatedly fit a small tree to the negative gradient of the loss and add a shrunken step of it to the running model. For squared-error regression that gradient is exactly the RESIDUAL y - F(x), so each tree learns what the ensemble still gets wrong; for logistic classification it is y - sigmoid(F(x)). Three knobs trade bias for variance: the number of trees adds capacity, the LEARNING RATE shrinks each tree's contribution (small rates need more trees but generalize better -- shrinkage is regularization), and tree DEPTH caps the feature interactions each weak learner captures. This module implements boosting for squared-error regression and log-loss binary classification over self-contained regression trees, with staged predictions, verified that training loss decreases monotonically as trees are added, that the ensemble beats a single tree by ~250x on a noisy target, that a smaller learning rate needs more trees, and that it separates a circular class boundary. This is the method that wins most tabular-data competitions.
Gradient boosting: shallow trees added in sequence, each fixing the last's errors
regression on 70 noisy points of sin(x) + 0.3x
final R^2 0.9997, MSE 0.0003
Training loss falls monotonically as trees are added:
after 1 trees: MSE 0.7882
after 2 trees: MSE 0.6447
after 5 trees: MSE 0.3595
after 10 trees: MSE 0.1474
after 30 trees: MSE 0.0083
after 60 trees: MSE 0.0017
after 120 trees: MSE 0.0003
A single depth-3 tree: MSE 0.0826 -- boosting is 252x better.
Learning rate vs number of trees (shrinkage regularizes):
lr=0.02 trees= 10: MSE 0.6537
lr=0.02 trees= 40: MSE 0.2198
lr=0.02 trees=120: MSE 0.0181
lr=0.1 trees= 10: MSE 0.1474
lr=0.1 trees= 40: MSE 0.0035
lr=0.1 trees=120: MSE 0.0003
lr=0.5 trees= 10: MSE 0.0031
lr=0.5 trees= 40: MSE 0.0001
lr=0.5 trees=120: MSE 0.0000
Classification on a circular boundary (a nonlinear problem for a linear model):
accuracy 99.5%, log-loss 0.113
Boosting is gradient descent in function space: each tree fits the residual (the
negative gradient of the loss), and a shrunken step of it is added to the model. Small
learning rates plus many shallow trees is the recipe that wins tabular competitions.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gradient_boosting.svgSpectral clustering: cutting a graph by its Laplacian
k-means splits space by distance to a centroid, so it fails on non-convex shapes -- concentric rings, interlocking moons. Spectral clustering escapes that by working on a GRAPH: connect nearby points with weighted edges, then cut the graph into pieces dense inside and sparse between. Remarkably, that combinatorial cut is solved (relaxed) by linear algebra -- the eigenvectors of the graph LAPLACIAN. Build a Gaussian affinity matrix, form the normalized Laplacian L = I - D^-1/2 W D^-1/2, take the eigenvectors of its k smallest eigenvalues (the number near zero equals the number of connected components), embed each point by its coordinates there, and run k-means in that space -- where the tangled shapes become tight, linearly separable blobs. This module builds the affinity graph and Laplacians, extracts the low eigenvectors by reusing a symmetric eigensolver on cI - L (turning smallest into largest), and clusters the embedding, verified to separate concentric rings and two moons that k-means cannot, and that the Laplacian's zero-eigenvalue multiplicity counts the graph's connected components. Built on the eigen and k-means modules.
Spectral clustering: cutting a graph by its Laplacian eigenvectors 44 points on two concentric rings (inner radius 1, outer 3) spectral clustering separates the rings: True plain k-means separates the rings: False (it can't -- centroids can't wrap a ring) Laplacian's smallest eigenvalues: [0.0, 0.0007] Two near-zero values => two clusters; the theorem: zero-eigenvalue multiplicity equals the number of connected components of the affinity graph. In the 2-eigenvector embedding the rings collapse to two tight point clouds -- k-means in that space is trivial. That is the whole trick: solve a hard geometric clustering by an easy one after a spectral change of coordinates. Sanity check: 3 far-apart blobs -> 3 graph components (and 3 zero Laplacian eigenvalues). wrote C:\Users\acwic\symplectic-nbody\examples\output\spectral_clustering.svg
Particle filters: nonlinear, non-Gaussian tracking
The Kalman filter is optimal but only for LINEAR dynamics with GAUSSIAN noise. When the motion or sensor is nonlinear, or the belief is multimodal, its single Gaussian breaks. A particle filter drops that assumption: it represents the belief as a CLOUD of weighted samples and propagates them through the true, arbitrary dynamics -- sequential Monte Carlo. Each step PREDICTS (push every particle through the motion model plus noise), WEIGHTS (reweight by the likelihood of the actual measurement), and RESAMPLES (draw a new equal-weight set in proportion to the weights, killing unlikely particles and duplicating likely ones). Resampling is the crux: without it a few particles hoard all the weight (DEGENERACY) and the cloud stops representing the posterior. The EFFECTIVE SAMPLE SIZE 1/sum(w^2) measures that, and we resample only when it drops below N/2, using low-variance systematic resampling. This module implements a generic bootstrap filter with adaptive systematic resampling, verified on a nonlinear tracking problem: its estimate beats the raw sensor by ~60%, resampling keeps the effective sample size high where a weight-only filter collapses to a single particle, and more particles reduce the error. Built with its own Gaussian sampler.
Particle filter: sequential Monte-Carlo estimation of a nonlinear system
50 steps, nonlinear motion x <- x + 0.3 sin(x) + 0.5, noisy position sensor
mean absolute error vs truth: raw measurements 0.754
particle filter 0.293 (61% better)
Effective sample size (of 500) -- resampling is what stops degeneracy:
adaptive resampling: min ESS 212, 10 resamples
NO resampling: min ESS 1.1 (cloud collapses to ~1 particle)
More particles, lower error (mean over 3 seeds):
20 particles: error 0.333
100 particles: error 0.303
500 particles: error 0.297
Unlike the Kalman filter, a particle filter needs no linearity or Gaussian noise --
it just pushes a cloud of samples through the true dynamics, reweights by the
measurement likelihood, and resamples to concentrate on where the posterior lives.
wrote C:\Users\acwic\symplectic-nbody\examples\output\particle_filter.svgSimulated annealing: escaping local minima by cooling
Greedy search only moves downhill and gets trapped in the first local minimum. Simulated annealing escapes by sometimes moving UPHILL, with a probability that shrinks over time -- the metallurgical analogy: heat a metal and cool it slowly so its atoms settle into a low-energy crystal, not a brittle freeze. At each step it proposes a random neighbour and applies the METROPOLIS criterion: a move lowering the cost is always taken; one raising it by delta is taken with probability exp(-delta/T). High T (early) accepts almost anything and roams freely out of local basins; as T cools only improving moves survive. The COOLING SCHEDULE is the key knob -- cool too fast and you quench into a poor minimum; cool slowly (geometric T <- alpha T) and you approach the global optimum. This module implements generic annealing over any state plus a travelling-salesman solver with 2-opt segment-reversal moves, verified to find the global minimum of a multimodal function that greedy descent misses, converge a square TSP tour to its exact optimal perimeter, beat nearest-neighbour greedy on random tours, and shrink its acceptance rate as it cools.
Simulated annealing: global optimization by cooling a fictitious temperature
Multimodal 1-D function (many local minima):
SA found x = -0.761, cost -4.650
true global x = -0.760, cost -4.650
acceptance rate over the run: 24.6% (high early, near zero once cold)
Travelling salesman, 25 random cities:
nearest-neighbour greedy tour: length 53.49
simulated-annealing tour: length 45.61 (15% shorter)
Tour length as the system cools (early rises = escaping local minima):
0% through: length 148.13
5% through: length 105.82
15% through: length 56.30
40% through: length 45.61
70% through: length 45.61
100% through: length 45.61
Annealing accepts uphill moves with probability exp(-delta/T): at high T it roams
freely and hops out of local basins; as T cools only improving moves survive and it
settles. Cool slowly enough and it approaches the global optimum -- here a far shorter
tour than greedy, and the true minimum of a function that traps hill-climbing.
wrote C:\Users\acwic\symplectic-nbody\examples\output\simulated_annealing.svgGenetic algorithms: optimization by simulated evolution
Where simulated annealing perturbs a single state, a genetic algorithm evolves a whole POPULATION, letting good solutions breed -- a direct metaphor for natural selection. Each generation SELECTS parents biased toward high fitness (tournament: the best of k random individuals), applies CROSSOVER to splice two parents' genes into offspring, MUTATES to inject new variation, and keeps the best few via ELITISM so the best-so-far never regresses. Being population-based it explores many basins at once and needs no gradients, making it strong on rugged, discrete, or black-box landscapes. This module implements a generic GA (tournament selection, one-point crossover, elitism) over both binary and real-valued genomes plus a 0/1 knapsack solver, verified to solve the OneMax bit problem to all-ones, maximize a multimodal real function, match the brute-force optimum of a small knapsack, keep the best fitness monotone under elitism, and beat random search at equal budget.
Genetic algorithm: optimization by simulated evolution
OneMax (maximize 1-bits in a 60-bit genome):
best fitness 60/60, reached all-ones: True
population mean fitness climbed 29.8 -> 58.5
Bumpy real function (many local optima):
GA found x = -0.761, fitness 4.650
true global x = -0.760, fitness 4.650
0/1 knapsack (capacity 20):
chose items [1, 2, 3, 5, 6], total value 28, total weight 20
Selection biases reproduction toward fit individuals; crossover recombines their
genes; mutation adds new variation; elitism preserves the best. The population
explores many basins at once -- no gradients, works on discrete or black-box problems.
wrote C:\Users\acwic\symplectic-nbody\examples\output\genetic_algorithm.svgParticle swarm optimization: a flock homing on the optimum
A third metaheuristic beside simulated annealing (one state) and genetic algorithms (breeding a population): a SWARM of candidate solutions flies through the search space, each remembering its own best spot and pulled toward the best any member has found -- inspired by flocking birds. Each particle carries a position and a VELOCITY updated by three pulls: INERTIA (coast on the old velocity, exploring), COGNITIVE (toward this particle's own best, individual memory), and SOCIAL (toward the swarm's global best, shared knowledge), with fresh randoms keeping it stochastic. High inertia explores, low inertia exploits, so it is often decayed over the run. No gradients, just local rules, and the swarm balances exploration against convergence. This module implements PSO over a bounded box with velocity clamping and linearly-decaying inertia, verified to find the global minimum of the Sphere, Rastrigin, and Rosenbrock benchmarks, drive the global best down monotonically, beat random search at equal budget, and converge faster when inertia decays.
Particle swarm optimization: a flock of solutions homing on the optimum
Sphere (min at origin): cost 0.00000 at (0.000, 0.000)
Rastrigin (min at origin): cost 0.00000 at (0.000, -0.000)
Rosenbrock (min at (1,1)): cost 0.00000 at (1.000, 1.000)
PSO vs random search at equal budget (40x300 = 12000 evaluations):
Sphere: PSO 0.00000 random 0.00001
Rastrigin: PSO 0.00000 random 0.00166
Rastrigin convergence (global best cost) -- decaying inertia converges faster:
iter 0: decaying-w 7.507 fixed-w 7.507
iter 10: decaying-w 0.485 fixed-w 0.010
iter 30: decaying-w 0.161 fixed-w 0.002
iter 60: decaying-w 0.001 fixed-w 0.002
iter 120: decaying-w 0.000 fixed-w 0.001
iter 199: decaying-w 0.000 fixed-w 0.000
Each particle coasts on inertia, is pulled toward its own best spot (cognitive) and
the swarm's best (social); r1,r2 keep it stochastic. High inertia explores, low
inertia exploits -- decaying it does both in turn. No gradients, just local rules.
wrote C:\Users\acwic\symplectic-nbody\examples\output\particle_swarm.svgReed-Solomon codes: recovering data from errors
The error-correcting code behind QR codes, CDs, DVDs, and deep-space probes. Reed-Solomon treats a message as the coefficients of a polynomial over the finite field GF(256) and appends 2t parity symbols so the whole codeword is divisible by a fixed generator polynomial. Any corruption breaks that divisibility in a way that pinpoints both WHERE the errors are and WHAT they should have been -- correcting up to t byte-errors per block no matter how they are distributed, which is why it survives a scratch on a disc or a fading radio link. The field is GF(2^8): bytes with XOR as addition and multiplication modulo 0x11d, so every nonzero byte is a power of the generator and multiplication is log-table addition. Decoding is the classic pipeline: SYNDROMES (evaluate at the code roots), BERLEKAMP-MASSEY (the error-locator polynomial), a CHIEN search (its roots = error positions), and FORNEY's formula (the error magnitudes). This module implements GF(256) arithmetic, encoding, and full syndrome decoding, verified that a clean codeword is unchanged, that up to t corrupted bytes anywhere (including in the parity) are corrected exactly across dozens of random trials, and that one error past the limit is flagged rather than mis-corrected.
Reed-Solomon error correction over GF(256) message: 'REED-SOLOMON' (12 bytes) parity : 8 bytes -> corrects up to t = 4 byte-errors per block codeword (20 bytes): 52 45 45 44 2d 53 4f 4c 4f 4d 4f 4e b2 24 3b b8 39 e9 27 8a corrupting 4 bytes at positions [2, 3, 9, 13] (a scratch / noise burst): received (20 bytes): 52 45 1f bb 2d 53 4f 4c 4f 4c 4f 4e b2 5a 3b b8 39 e9 27 8a syndromes (nonzero => error detected): nonzero -> [218, 230, 113, 0]... decoded: corrected 4 byte-errors recovered codeword matches original: True recovered message: 'REED-SOLOMON' (correct: True) 12+8 block with 5 errors (one past the limit): correctly flagged as uncorrectable. A message is the coefficients of a polynomial over GF(256); parity makes the codeword divisible by the generator. Corruption breaks that divisibility, and the syndromes -> Berlekamp-Massey -> Chien -> Forney pipeline recovers both the error positions and their values. This is the code in QR codes, CDs, and deep-space telemetry. wrote C:\Users\acwic\symplectic-nbody\examples\output\reed_solomon.svg
Mutual information: measuring dependence between variables
Entropy measures the uncertainty in one variable; MUTUAL INFORMATION I(X;Y) measures how much knowing one reduces the uncertainty in the other -- the shared information. Unlike correlation it captures ANY relationship, linear or not: I = 0 exactly when X and Y are independent, and it equals H(X) for a deterministic copy. Everything is built from Shannon entropy: I(X;Y) = H(X) + H(Y) - H(X,Y) = H(X) - H(X|Y). The Kullback-Leibler divergence D(p||q) = sum p log2(p/q) -- the extra bits to code p-samples with a q-code -- is the asymmetric distance from which MI is the divergence of the joint from the product of marginals, and INFORMATION GAIN (the decision-tree split criterion) is exactly the mutual information between a feature and the label. This module estimates entropies and mutual information from samples or a joint distribution with KL divergence and normalized MI, verified that independent variables have zero MI, a copy has I = H (maximal), the H(X)+H(Y)-H(X,Y) and conditional-entropy identities hold, KL is nonnegative and zero only for equal distributions, and against hand-computed values -- and it catches a nonlinear dependence that correlation reports as ~0.
Mutual information: how much one variable tells you about another
Binary symmetric channel, Y = X flipped with probability f:
(I falls from H(X)=1 bit at f=0 to 0 at f=0.5, the fully-noisy channel)
f = 0.00: I(X;Y) = 1.000 bits ########################################
f = 0.05: I(X;Y) = 0.712 bits ############################
f = 0.10: I(X;Y) = 0.542 bits ######################
f = 0.20: I(X;Y) = 0.287 bits ###########
f = 0.30: I(X;Y) = 0.123 bits #####
f = 0.40: I(X;Y) = 0.027 bits #
f = 0.50: I(X;Y) = 0.000 bits
MI sees nonlinear dependence that (linear) correlation misses:
Y = X^2 (deterministic): correlation -0.002 (near 0, looks 'unrelated')
mutual information 1.522 bits (= H(X), fully dependent)
Feature selection -- rank features by information gain about the label:
informative: information gain 0.5054 bits
weak: information gain 0.0702 bits
noise: information gain 0.0003 bits
KL divergence D(p||q) -- extra bits to code p-samples with a q-code:
D([0.5,0.50] || fair) = 0.0000 bits
D([0.7,0.30] || fair) = 0.1187 bits
D([0.9,0.10] || fair) = 0.5310 bits
D([0.99,0.01] || fair) = 0.9192 bits
Mutual information is the shared information I(X;Y) = H(X)+H(Y)-H(X,Y): zero iff
independent, maximal for a deterministic relationship, and blind to nothing -- it
catches any dependency, which is why it drives feature selection and tree splits.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mutual_information.svgLRU & LFU caches: O(1) eviction policies
A cache holds a fixed number of items and must EVICT one when full; which one decides the hit rate. LRU (Least Recently Used) evicts the item untouched longest, betting on temporal locality -- the default in CPU caches, page tables, and web caches. LFU (Least Frequently Used) evicts the least-accessed, betting popularity persists. The craft is doing it in O(1): a naive LRU scans for the oldest item on every eviction, but a HASH MAP (key -> node) for lookup plus a DOUBLY-LINKED LIST ordered by recency makes touch-and-promote and tail-eviction O(1); LFU groups keys into frequency buckets so increments and min-frequency eviction are O(1) amortized. This module implements both with hit/miss statistics, verified that LRU evicts in true least-recently-used order (checked against a brute-force reference over 60 random workloads), that touching an item spares it, that LFU evicts the least-frequent breaking ties by recency, that capacity is never exceeded, and that a skewed hot-key workload gives LFU a higher hit rate than LRU.
LRU / LFU caching: which item to evict when memory is full
LRU trace (capacity 3), MRU-to-LRU order after each op:
put A -> [A]
put B -> [B, A]
put C -> [C, B, A]
get A (hit) -> [A, C, B]
put D -> [D, A, C]
get C (hit) -> [C, D, A]
put E -> [E, C, D]
(D's insertion evicted B, the least-recently-used; E evicted A)
Hit rate by workload (cache holds 10 of 200 keys):
uniform random : LRU 4.5% LFU 5.0%
skewed (hot keys) : LRU 22.7% LFU 31.5%
looping sweep : LRU 0.0% LFU 0.0%
No policy wins everywhere: LFU shines when a few keys are truly hot (it remembers
popularity), LRU adapts faster to shifting working sets. Both do get/put in O(1) --
LRU via a hash map plus a recency-ordered doubly-linked list, LFU via frequency buckets.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lru_cache.svgTries: the prefix tree behind autocomplete
A trie stores strings as a tree where each edge is a character and each root-to-node path spells a prefix. Words sharing a prefix share its path, so the structure is naturally compressed by common beginnings and every operation runs in O(length of the key) -- INDEPENDENT of how many keys are stored, unlike a hash set's collisions or a balanced tree's O(log n) whole-string comparisons. That per-character walk is exactly what powers autocomplete (every word under a prefix), longest-prefix matching (IP routers), and dictionary spell-check. Deletion unmarks a word and prunes now-childless non-terminal nodes on the way back up, and building a trie over all SUFFIXES of a text turns it into a substring index -- any substring is a prefix of some suffix. This module implements insert, search, prefix membership, autocomplete (alphabetical or frequency-ranked), deletion with pruning, longest-prefix matching, and a suffix-trie substring index, verified that it distinguishes a stored word from a mere prefix, autocompletes exactly the words under a prefix, deletes without disturbing siblings or shared prefixes, and detects substrings and their positions.
Trie: prefix tree for autocomplete and O(len) lookup
dictionary: 10 distinct words, 39 total insertions
Autocomplete 'the' (alphabetical):
['the', 'their', 'them', 'there', 'they']
Autocomplete 't' by insertion frequency (what a search box would rank):
the (typed 9x)
to (typed 8x)
there (typed 5x)
their (typed 4x)
they (typed 3x)
membership: 'the' stored = True, 'th' stored = False (prefix only)
starts_with 'thei' = True, 'xyz' = False
longest stored prefix of 'thereafter' = 'there'
Deletion prunes dead branches but spares shared prefixes:
after deleting 'tea': autocomplete 'te' -> ['ten'] (ten kept)
Suffix-trie substring index of 'abracadabra':
'abra': present at [0, 7]
'cad': present at [4]
'bra': present at [1, 8]
'xyz': not found
Every operation is O(length of the key), independent of how many words are stored --
words sharing a prefix share its path. That per-character walk is what powers
autocomplete, spell-check, and longest-prefix IP routing.
wrote C:\Users\acwic\symplectic-nbody\examples\output\trie.svgComparison sorts: the classic four and their trade-offs
Sorting is computing's most-studied problem, and the classic comparison algorithms each make a different trade among speed, memory, stability, and worst case -- all bounded below by the O(n log n) decision-tree limit. INSERTION sort is O(n^2) but fast on nearly-sorted data and the base case big sorts fall back to; MERGE sort is O(n log n) ALWAYS and stable but needs O(n) scratch; QUICK sort is usually fastest and in-place but O(n^2) on adversarial input unless the pivot is chosen well (here median-of-three plus an insertion cutoff); HEAP sort is O(n log n) worst-case AND in-place, built on a binary heap that doubles as a priority queue. This module implements all four plus the heap with a comparison counter and a key function, verified that every sort matches Python's built-in on random, sorted, reverse, and duplicate-heavy inputs, that merge and insertion are stable while quick and heap are not, that comparison counts scale as O(n log n) for the good sorts and O(n^2) for insertion, that median-of-three keeps quick sort fast on its classic sorted/reverse adversaries, and that the heap is a correct priority queue.
Comparison sorts: speed, stability, and worst-case trade-offs
All four match Python's sorted() exactly; they differ in HOW they get there:
algorithm time stable in-place note
insertion O(n^2) yes yes great on nearly-sorted / tiny arrays
merge O(n log n) yes no O(n) scratch; never degrades
quick O(n log n)* no yes *O(n^2) worst; median-of-3 avoids it
heap O(n log n) no yes worst-case AND in-place
Comparisons vs input size (random data):
n insertion merge quick heap
100 2554 544 642 1016
200 9545 1277 1552 2460
400 42183 2958 3784 5728
800 159812 6725 8665 13033
1600 647433 15071 17964 29259
(insertion's column grows ~4x when n doubles = O(n^2); the rest ~2x = O(n log n))
Stability -- sort (key, original-index) pairs by key; do equal keys keep their order?
insertion : stable (0 reorderings)
merge : stable (0 reorderings)
quick : UNstable (10 reorderings)
heap : UNstable (16 reorderings)
The binary heap behind heap sort is also a priority queue:
popped by priority: ['fire', 'bug', 'email', 'meeting', 'lunch']
Every comparison sort is bounded below by O(n log n); the interesting differences are
the constants, the memory (merge needs scratch, heap and quick don't), stability, and
the worst case (merge and heap never degrade; quick can without a good pivot).
wrote C:\Users\acwic\symplectic-nbody\examples\output\sorting.svgNewton's method in n dimensions: solving nonlinear systems
One-dimensional Newton iterates x <- x - f/f'; in n dimensions the derivative becomes the JACOBIAN matrix and the division becomes solving a linear system J(x) delta = -F(x), then x <- x + delta. Each step linearizes the system at the current point, jumps to that linear model's root, and repeats -- and near a solution it converges QUADRATICALLY, the number of correct digits roughly doubling each step. It is the engine inside circuit simulation, inverse kinematics, chemical equilibrium, and optimization. When the analytic Jacobian is unavailable it is approximated by finite differences; because plain Newton can overshoot and diverge far from a root, a damped line search backtracks the step until the residual actually decreases for global robustness; and a Broyden quasi-Newton mode updates a Jacobian approximation instead of recomputing it. This module implements all three, each solving the linear step via LU with partial pivoting, verified on a circle-line intersection and the Rosenbrock stationary point, that convergence is quadratic near the root, that the finite-difference Jacobian matches an analytic one, that damping rescues a start where plain Newton diverges (arctan from far out), and that Broyden converges too.
Newton's method in n dimensions: J(x) delta = -F(x), x <- x + delta
System: x^2 + y^2 = 4 and y = x -> root (sqrt2, sqrt2) = (1.414214, 1.414214)
Newton from (1.6, 2.4): converged in 5 iterations to (1.41421356, 1.41421356)
residual norm per step (note the digits roughly DOUBLING -- quadratic convergence):
step 0: |F| = 4.39e+00
step 1: |F| = 7.43e-01
step 2: |F| = 2.91e-02
step 3: |F| = 5.26e-05
step 4: |F| = 1.73e-10
step 5: |F| = 8.88e-16
Broyden quasi-Newton from the same start: 7 iterations (no Jacobian recompute per step; a bit slower to converge)
Global robustness -- solving arctan(x) = 0 from a far start x0 = 5:
plain Newton: diverged (overshoots, |x| explodes)
damped Newton: converged to 0.000000 in 7 iterations (backtracks until the residual drops)
3-variable system (sum 6, sq-sum 14, product 6): root (1.0000, 2.0000, 3.0000) = (1, 2, 3)
Each step linearizes the system at the current point and jumps to that linear model's
root; near a solution the error squares each iteration. Far away it can overshoot, so a
line search that backtracks until the residual falls buys global robustness. This is
the solver inside circuit simulation, inverse kinematics, and chemical equilibrium.
wrote C:\Users\acwic\symplectic-nbody\examples\output\newton_nd.svgDifferential evolution: optimization by vector differences
A population optimizer for continuous spaces that, unlike genetic algorithms (mutating bits) or particle swarms (tracking velocities), mutates by ADDING SCALED DIFFERENCES between population members. That self-referential step is its signature: the spread of the population itself sets the mutation scale, so the search takes large steps while dispersed (early, exploring) and small ones as it converges (late, refining) -- no cooling schedule or velocity tuning. The classic DE/rand/1/bin per target: pick three other members and form a donor v = a + F*(b - c); build a trial by taking each coordinate from the donor with probability CR (else from the target); keep whichever of trial and target has the lower cost (greedy selection, so the best never worsens). This module implements DE/rand/1/bin over a bounded box with bound reflection and a random-search baseline, verified that it finds the global minimum of the Sphere, Rastrigin, and Rosenbrock benchmarks, that the best cost is monotone, that it beats random search at equal budget, that the solution stays in bounds, and that it scales to 10-20 dimensions. One of the most robust black-box optimizers for continuous problems.
Differential evolution: DE/rand/1/bin -- donor = a + F*(b - c)
Sphere (min at origin): cost 0.000000 at (-0.000, 0.000)
Rastrigin (min at origin): cost 0.000000 at (-0.000, -0.000)
Rosenbrock (min at (1,1)): cost 0.000000 at (1.000, 1.000)
DE vs random search at equal budget (40x200 = 8000 evaluations):
Sphere: DE 0.000000 random 0.004577
Rastrigin: DE 0.000000 random 0.938405
Rosenbrock: DE 0.000000 random 0.005118
Differential weight F trades exploration for refinement (Rastrigin, 60 iters):
F = 0.3: best cost after 60 iters = 0.0000
F = 0.5: best cost after 60 iters = 0.0000
F = 0.7: best cost after 60 iters = 0.0015
F = 0.9: best cost after 60 iters = 0.0001
Scales to higher dimensions (Sphere):
2-D: cost 0.000000
5-D: cost 0.000000
10-D: cost 0.000000
20-D: cost 0.930333
DE mutates by adding a SCALED DIFFERENCE between population members, so the
population's own spread sets the step size -- large while dispersed (exploring), small
as it converges (refining), with no schedule to tune. Greedy selection keeps the best
monotone. It is one of the most robust black-box optimizers for continuous problems.
wrote C:\Users\acwic\symplectic-nbody\examples\output\differential_evolution.svgNelder-Mead: derivative-free optimization by a crawling simplex
Newton needs a Jacobian and gradient descent a gradient; Nelder-Mead needs NEITHER. It minimizes using only function VALUES, maintaining a simplex of n+1 points (a triangle in 2-D, a tetrahedron in 3-D) that tumbles and shrinks downhill -- the workhorse behind 'just minimize this black box' (SciPy's gradient-free default, MATLAB's fminsearch). Each iteration reflects the worst vertex through the centroid of the others and, by how good that reflection is, EXPANDS further in a promising direction, CONTRACTS back toward the centroid, or SHRINKS the whole simplex toward the best vertex. The simplex crawls like an amoeba, stretching down valleys and squeezing through the curved Rosenbrock banana. This module implements the standard algorithm with the classic coefficients, value- and size-based convergence, and restarts, verified that it finds the minimum of the Sphere, Rosenbrock, and Beale benchmarks from several starts with no gradient, that the best vertex improves monotonically, that it even minimizes a non-smooth objective, and that restarting refines the result.
Nelder-Mead: derivative-free minimization by a crawling simplex
Uses ONLY function values -- no gradient, no Jacobian. A simplex of n+1 points
reflects, expands, contracts, and shrinks its way downhill.
Sphere from [3.0, -2.0]: min 1.35e-21 at (0.0000, 0.0000) [true min at (0,0)] 80 iters, 156 evals
Rosenbrock from [-1.2, 1.0]: min 7.65e-22 at (1.0000, 1.0000) [true min at (1,1)] 135 iters, 261 evals
Beale from [1.0, 1.0]: min 1.44e-22 at (3.0000, 0.5000) [true min at (3,0.5)] 89 iters, 170 evals
Rosenbrock best-vertex value as the simplex crawls the banana valley:
iter 0: 5.200e+00
iter 5: 4.604e+00
iter 20: 1.947e+00
iter 50: 1.850e-01
iter 100: 1.666e-12
iter 134: 7.647e-22
Restarting the simplex refines the result: single run 7.89e-22 -> 3 restarts 7.89e-22
Cost of no derivatives -- function evaluations grow with dimension (Sphere):
2-D: 144 evaluations to converge
4-D: 361 evaluations to converge
8-D: 1143 evaluations to converge
16-D: 2848 evaluations to converge
The simplex tumbles and stretches like an amoeba: reflecting the worst vertex
through the others, expanding into promising directions, contracting when it
overshoots, shrinking when stuck. Robust and gradient-free -- the default when all
you can do is evaluate the function.
wrote C:\Users\acwic\symplectic-nbody\examples\output\nelder_mead.svgHITS: hubs and authorities
PageRank's contemporary rival, and it splits importance into TWO complementary scores. An AUTHORITY is a page many good hubs point to (a definitive source); a HUB is a page that points to many good authorities (a good list of links). The definitions are mutually recursive -- a good authority is linked by good hubs, a good hub links to good authorities -- resolved by iterating to a fixed point: authority(p) = sum of hub scores linking to p, hub(p) = sum of authority scores p links to, normalized each round. Written with the adjacency matrix A the update is a = A'h, h = Aa, so authorities are the dominant eigenvector of A'A and hubs of AA' -- HITS is power iteration on those matrices. Unlike PageRank's single query-independent score, HITS yields the two roles separately, so a curated link list and the source everyone cites rank differently. This module computes HITS by power iteration with normalization plus the eigenvector check, verified that on a hub-and-spoke graph the hub score flags the linker and the authority score the linked-to targets, that scores converge and are unit-normalized, that a pure authority has zero hub score and vice versa, and that the results match the dominant eigenvectors of A'A and AA'.
HITS: hubs and authorities (two scores per node)
8 pages, 14 links
Top HUBS (good lists of links -- point to many authorities):
portal: 0.630
reader: 0.536
student: 0.367
fan: 0.263
Top AUTHORITIES (definitive sources -- linked by many good hubs):
wiki: 0.631
news: 0.519
journal: 0.498
blog: 0.290
Hubs and authorities are DIFFERENT roles -- compare the two rankings and PageRank:
page hub authority pagerank
wiki 0.000 0.631 0.369
news 0.205 0.519 0.192
journal 0.205 0.498 0.111
blog 0.169 0.290 0.095
fan 0.263 0.000 0.058
portal 0.630 0.000 0.058
reader 0.536 0.000 0.058
student 0.367 0.000 0.058
Best hub is 'portal' (a link directory); best authority is 'wiki' (what everyone cites).
PageRank collapses this into one score; HITS keeps the two roles distinct.
A good authority is pointed to by good hubs; a good hub points to good authorities.
That mutual recursion is resolved by power iteration -- authorities are the dominant
eigenvector of A'A, hubs of AA'. Kleinberg's HITS ran per query; PageRank runs once
globally. Both turn the link graph into a ranking by linear algebra.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hits.svgClassical MDS: a map from a table of distances
Given only the pairwise DISTANCES between objects -- cities on a map, dissimilar survey responses, aligned sequences -- multidimensional scaling reconstructs COORDINATES whose distances match, answering 'where do these sit relative to each other?' from distances alone. Classical (Torgerson) MDS solves it in closed form by DOUBLE CENTERING: from the squared-distance matrix, B = -1/2 J D2 J turns distances into the centered inner-product (Gram) matrix B = X X', and eigendecomposing B = V L V' gives the coordinates X = V L^{1/2} -- the top k eigenvectors scaled by the square roots of their eigenvalues, with the eigenvalues themselves reporting how much shape each dimension carries. The map is unique only up to rotation, reflection, and translation. This module builds the squared-distance and double-centered matrices, extracts the embedding by eigendecomposition, reports the eigenvalue spectrum, and Procrustes-aligns a reconstruction to a known map, verified that it recovers a square, a line, and random point sets so their reconstructed distances match, that a flat configuration has exactly two positive eigenvalues, and that the stress is essentially zero for Euclidean inputs. Built on the eigen module.
Classical MDS: reconstructing a map from a distance table
6 cities; input is ONLY the 6x6 distance matrix.
distance table (rounded):
A B C D E F
A 0.00 4.03 6.10 4.47 2.55 3.20
B 4.03 0.00 3.16 4.03 4.03 2.12
C 6.10 3.16 0.00 3.04 4.61 2.92
D 4.47 4.03 3.04 0.00 2.12 2.06
E 2.55 4.03 4.61 2.12 0.00 2.06
F 3.20 2.12 2.92 2.06 2.06 0.00
Recovered a 2-D embedding; stress (distance mismatch) = 2.43e-23
max pairwise-distance error after reconstruction = 2.86e-12
Eigenvalue scree (how much 'shape' each axis carries -- flat map => 2 positive):
axis 1: 21.283 ##############################
axis 2: 10.258 ##############
axis 3: 0.000
axis 4: 0.000
axis 5: 0.000
axis 6: -0.000
-> 2 clearly-positive eigenvalues = intrinsic dimensionality 2
Procrustes-aligned recovered coordinates vs the true map:
city true (x,y) recovered (x,y)
A ( 0.00, 0.00) (-0.00, 0.00)
B ( 4.00, 0.50) ( 4.00, 0.50)
C ( 5.00, 3.50) ( 5.00, 3.50)
D ( 2.00, 4.00) ( 2.00, 4.00)
E ( 0.50, 2.50) ( 0.50, 2.50)
F ( 2.50, 2.00) ( 2.50, 2.00)
Double-centering the squared-distance matrix turns distances into inner products;
its top eigenvectors, scaled by sqrt(eigenvalue), are the coordinates. The map is
recovered up to rotation, reflection, and translation -- distances fix shape, not
orientation -- so Procrustes rotates the reconstruction back onto the known layout.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mds.svgSkip lists: a probabilistic ordered dictionary
A balanced binary search tree gives O(log n) search, insert, and delete but needs intricate rotations; a SKIP LIST reaches the same expected bounds with almost no bookkeeping, using RANDOMNESS instead. It is an ordered linked list with EXPRESS LANES: each node is promoted to the next level up with probability p (~1/2), so level 0 holds every element, level 1 about half, level 2 a quarter -- a tower of sparser and sparser shortcut lists. A search drops down from the top lane, skipping far along each level before descending, covering the list in O(log n) expected hops. Because promotion is a coin flip there are no rotations: insert picks a random height and splices in, delete unlinks, and the structure stays probabilistically balanced on its own (Redis sorted sets use it). This module implements a skip-list ordered map with insert, search, delete, ordered iteration, range queries, and min/max, verified against a brute-force sorted dictionary over 4000 random operations (every search, deletion, and traversal agrees), that keys iterate sorted, that duplicates update rather than duplicate, that range queries return exactly the in-range keys, and that the level distribution is geometric as designed.
Skip list: an ordered map with express lanes for O(log n) search
10 keys: [3, 6, 7, 9, 12, 17, 19, 21, 25, 26]
The tower of shortcut lanes (level 0 has every key; higher lanes are sparser):
L4: 9
L3: 9 12
L2: 9 12 19
L1: 3 6 9 12 17 19 21 25
L0: 3 6 7 9 12 17 19 21 25 26
Searching for 21 (drop down from the top, skip far, descend):
L4: advanced to 9
L3: advanced to 12
L2: advanced to 19
found 21: True in 3 forward hops (a level-0 scan would take up to 7)
Level distribution over 4000 keys (geometric: ~half promoted each rung):
level 0: 1975 (49.4%) #########################
level 1: 1019 (25.5%) #############
level 2: 516 (12.9%) ######
level 3: 238 ( 5.9%) ###
level 4: 125 ( 3.1%) ##
level 5: 64 ( 1.6%) #
level 6: 30 ( 0.8%)
level 7: 14 ( 0.4%)
level 8: 11 ( 0.3%)
level 9: 5 ( 0.1%)
level 10: 1 ( 0.0%)
level 11: 1 ( 0.0%)
level 12: 1 ( 0.0%)
Each node is promoted to the next lane by a coin flip, so the lanes thin out
geometrically and a search covers the list in O(log n) expected hops -- the same
bound as a balanced tree, but with random splices instead of rotations. No
rebalancing: insert picks a random height and links in; delete unlinks. (Redis uses it.)
wrote C:\Users\acwic\symplectic-nbody\examples\output\skiplist.svgAnt colony optimization: pheromone trails for the travelling salesman
Real ants find short paths without a map: each lays a PHEROMONE trail, shorter paths get traversed sooner so their trails are reinforced first, and other ants prefer strongly-scented edges -- positive feedback that converges the colony onto good routes. Ant colony optimization turns that into a TSP solver. Each iteration a swarm of artificial ants each builds a tour, at every step choosing the next city with probability proportional to tau^alpha * eta^beta, where tau is the learned pheromone on an edge and eta = 1/distance is the greedy heuristic (alpha weights experience, beta greed). Then pheromone EVAPORATES (forgetting stale trails) and each ant DEPOSITS an amount inversely proportional to its tour length, so shorter tours leave stronger trails and the pheromone concentrates on good edges. This module implements ant system for the symmetric TSP with the standard transition rule, evaporation, length-weighted deposit, and elitist reinforcement, verified that it recovers the optimal perimeter of a square and a circle's polygon, beats the nearest-neighbour greedy tour on random cities, drives the best length down monotonically, and concentrates pheromone on short edges.
Ant colony optimization: pheromone trails solving the travelling salesman
22 cities, 120 iterations, colony of 22 ants
nearest-neighbour greedy tour: length 52.94
ant colony optimization: length 42.85 (19% shorter)
Best tour length as pheromone accumulates (early rounds explore, then converge):
iter 0: 57.21
iter 5: 43.48
iter 15: 43.48
iter 40: 43.23
iter 80: 42.85
iter 119: 42.85
Pheromone on the 22 best-tour edges holds 79% of all pheromone (of 231 possible edges) -- the colony has converged.
Pheromone (alpha) vs greedy heuristic (beta) balance -- final tour length:
pheromone only: 76.06
a=1.0, b=2.0: 42.85
a=1.0, b=5.0: 42.85
greedy only: 44.77
Each ant builds a tour choosing the next city by pheromone^alpha * (1/dist)^beta;
pheromone evaporates then is redeposited inversely to tour length, so short tours
reinforce their edges and the colony converges -- swarm intelligence, no central plan.
wrote C:\Users\acwic\symplectic-nbody\examples\output\ant_colony.svgAVL trees: a self-balancing binary search tree
A plain binary search tree degrades to a linked list -- O(n) operations -- if keys arrive sorted. An AVL tree (the first self-balancing BST) prevents that by keeping every node HEIGHT-BALANCED: its two subtrees' heights differ by at most 1. After each insert or delete it checks the balance factor up the path to the root and, wherever it exceeds the bound, restores it with a local ROTATION -- a constant-time pointer rewiring that shortens the tall side. Four cases cover every imbalance: left-left and right-right take a single rotation, left-right and right-left a double. The strict invariant makes AVL the most rigidly balanced classic BST (shorter than a red-black tree), so lookups are fast at the cost of a little more rotation on updates -- where a skip list stays balanced probabilistically, an AVL tree does so deterministically. This module implements an AVL ordered map with insert, delete, search, ordered traversal, range queries, and min/max, verified against a brute-force sorted dictionary over 5000 random operations, that the height-balance invariant holds throughout, that the height stays O(log n) even for sorted insertions (where a naive BST would be linear), and that all four rotation cases trigger.
AVL tree: a binary search tree that rotates itself back into balance
Inserting SORTED keys (the naive-BST worst case that degrades to a linked list):
n AVL height naive BST log2(n)
15 4 15 3.9
63 6 63 6.0
255 8 255 8.0
1023 10 1023 10.0
4095 12 4095 12.0
-> AVL height tracks log2(n); a naive BST would be n (a linear chain).
The four rotation cases, each fixing one imbalance shape:
insert [3, 2, 1] (LL, left-left -> right rotation): root rebalances to 2, balanced=True
insert [1, 2, 3] (RR, right-right-> left rotation): root rebalances to 2, balanced=True
insert [3, 1, 2] (LR, left-right -> left then right): root rebalances to 2, balanced=True
insert [1, 3, 2] (RL, right-left -> right then left): root rebalances to 2, balanced=True
Example tree of 10 keys: height 4, balanced=True, sorted keys [10, 20, 25, 30, 35, 40, 50, 60, 70, 80]
range [25, 60]: [25, 30, 35, 40, 50, 60]
After every insert or delete, the tree checks the balance factor up the path and,
where |left height - right height| > 1, rotates -- a constant-time pointer rewiring --
to restore balance. Deterministic O(log n) worst case, where a skip list gets there
probabilistically with coin flips instead of rotations.
wrote C:\Users\acwic\symplectic-nbody\examples\output\avl_tree.svgSegment trees with lazy propagation: range query and range update
A Fenwick tree answers prefix sums with point updates; a SEGMENT TREE handles arbitrary RANGE queries (sum, min, max, ...) AND RANGE updates -- add a value to every element in [l, r] -- all in O(log n). Each node stores the aggregate of a contiguous segment; the root covers everything and each node splits its range in half between two children, so a query descends only into the O(log n) nodes whose segments tile the range. The trick for range UPDATES is LAZY PROPAGATION: rather than touch every leaf (O(n)), a node records a pending update as a lazy tag, applies it to itself immediately, and pushes it down to its children only when a later operation actually visits them -- so a full-array update touches ~2 log n nodes, not n. This module implements a segment tree parameterized by the aggregate (sum, min, or max) with lazy range-add updates, point updates, and range queries, verified against a brute-force array over 3000 random mixed operations for all three aggregates, that overlapping range-adds accumulate correctly, that point updates match a plain list, and that it handles single-element and full-array edge ranges.
Segment tree with lazy propagation: O(log n) range query AND range update
array: [3, 1, 4, 1, 5, 9, 2, 6]
Range-SUM queries:
sum[0..7] = 31
sum[2..5] = 19
sum[0..0] = 3
sum[6..7] = 8
Range-ADD updates (lazy -- a full-array add touches only O(log n) nodes):
after +10 to [2..5]: array = [3, 1, 14, 11, 15, 19, 2, 6]
sum[0..7] = 71, sum[2..5] = 59
after +100 to [0..7]: sum[0..7] = 871
The same array under MIN and MAX aggregates:
[0..7] min = 1 max = 9
[1..3] min = 1 max = 4
[4..6] min = 2 max = 9
min[0..7] after +5 to [0..3]: 2
Lazy propagation keeps cost logarithmic -- a range-add over the WHOLE array:
array size 16: a full-range update visits ~10 nodes (not 16) -- O(log n)
array size 256: a full-range update visits ~18 nodes (not 256) -- O(log n)
array size 4096: a full-range update visits ~26 nodes (not 4096) -- O(log n)
array size 65536: a full-range update visits ~34 nodes (not 65536) -- O(log n)
Each node holds the aggregate of a segment; a query descends only into the O(log n)
nodes whose segments tile the range. A range update marks a 'lazy' tag on the covering
nodes and pushes it to children only when a later operation visits them -- so add-to-a-
window and query-a-window are both O(log n), which a Fenwick tree (point-update) can't do.
wrote C:\Users\acwic\symplectic-nbody\examples\output\segment_tree.svgConvex hull: the tightest polygon enclosing points
The convex hull is the smallest convex polygon containing a point set -- the shape a rubber band snaps to around a scatter of pins, and the foundation of computational geometry (collision detection, shape analysis, path-planning boundaries). This module builds it by ANDREW'S MONOTONE CHAIN in O(n log n): sort the points, then sweep left-to-right building the lower hull and right-to-left the upper, keeping only left turns. The engine is the CROSS PRODUCT, whose sign gives orientation -- (b-a) x (c-a) > 0 is a counter-clockwise turn -- so the chain pops any vertex that would make a non-left turn, leaving only the outer boundary. From the hull come the enclosed AREA (shoelace formula), perimeter, whether an arbitrary point lies inside (orientation tests against each edge), and the DIAMETER (farthest pair, which always lies on the hull). This module implements the hull, area, perimeter, point-in-hull, and diameter, verified that a square's hull is its four corners (interior points dropped), that collinear and duplicate points are handled, that the hull is convex and counter-clockwise, that its area matches an independent shoelace value, that every input point lies inside it, and that the diameter is the true farthest pair.
Convex hull: the tightest polygon enclosing a set of points
60 random points -> hull of 11 vertices (18% of the points are on the boundary)
hull is convex & counter-clockwise: True
every input point inside or on the hull: True
enclosed area: 8310.0
perimeter: 345.0
diameter (farthest pair): 123.9 between (8.2,5.2) and (94.3,94.2)
Hull vertices in counter-clockwise order:
( 0.2, 60.0)
( 8.2, 5.2)
( 21.2, 2.6)
( 64.2, 2.0)
( 85.9, 5.9)
( 96.1, 19.5)
( 94.3, 94.2)
( 91.7, 95.4)
( 18.6, 96.3)
( 14.5, 95.6)
( 7.9, 89.1)
Interior points are dropped -- only the extreme points survive:
square + centre: 5 points -> 4 hull vertices
collinear: 4 points -> 2 hull vertices
triangle + inside: 4 points -> 3 hull vertices
Andrew's monotone chain sorts the points, then sweeps building the lower and upper
hulls, keeping only left turns (positive cross product) and popping any vertex that
would make a right turn. O(n log n), dominated by the sort. The farthest pair always
lies on the hull, so the diameter needs only the hull vertices.
wrote C:\Users\acwic\symplectic-nbody\examples\output\convex_hull.svgClosest pair of points: divide and conquer beats O(n^2)
Finding the two closest of n points is trivially O(n^2); the elegant 1975 result is that divide and conquer does it in O(n log n) -- one of the first proofs that geometry could beat the brute-force quadratic. Sort by x, split into halves, recursively find the closest pair in each, and let d be the smaller distance. The only remaining candidates straddle the split line and must lie within a vertical STRIP of width 2d around it -- and there, sorted by y, each point can be closer than d to at most a constant number (7) of following points, because a d x 2d rectangle holds only so many points that are all >= d apart. So the merge is linear and T(n) = 2T(n/2) + O(n) = O(n log n). This module implements the divide-and-conquer closest pair with the strip merge plus the brute-force reference, verified that the two agree exactly on random sets of many sizes, that it finds a planted near-coincident pair, handles duplicate points (distance 0), collinear and grid inputs, small n, and survives a crowded strip that would trap a naive merge.
Closest pair of points: O(n log n) divide and conquer
80 random points in a 100x100 square
closest pair: (37.39, 56.14) and (38.08, 56.79)
distance: 0.9444
brute-force agrees: True (distance 0.9444)
Distance computations vs input size (divide-and-conquer beats O(n^2)):
n brute O(n^2) D&C (approx)
16 120 64
64 2016 384
256 32640 2048
1024 523776 10240
4096 8386560 49152
-> at n=4096, brute does ~8.4M comparisons; D&C ~49K, a ~170x saving.
Divide-and-conquer matches brute force exactly:
n= 10: closest 92.7770 (brute 92.7770, match True)
n= 50: closest 19.1854 (brute 19.1854, match True)
n= 200: closest 5.5590 (brute 5.5590, match True)
n= 800: closest 1.1317 (brute 1.1317, match True)
Split the x-sorted points in half, recurse, take the smaller distance d, then check
only the pairs inside the width-2d strip around the split line -- where, sorted by y,
each point can beat d against at most a constant number of neighbours. Linear merge,
so T(n) = 2T(n/2) + O(n) = O(n log n): geometry beating the quadratic.
wrote C:\Users\acwic\symplectic-nbody\examples\output\closest_pair.svgLine-segment intersection: do two segments cross, and where?
Deciding whether two segments intersect -- and finding the point -- is the atom of computational geometry: collision detection, map overlays, clipping. The robust test avoids slopes (which blow up for vertical lines) and uses the ORIENTATION of point triples from the sign of a cross product: orient(a,b,c) is +1 counter-clockwise, -1 clockwise, 0 collinear. Two segments PROPERLY cross when each straddles the other's line (its endpoints have opposite orientations); the fiddly COLLINEAR/TOUCHING cases give a zero orientation and are settled by an on-segment bounding-box check -- handling them is what separates a toy from a usable predicate. The crossing point comes from the 2x2 parametric system. From this atom the module builds the SIMPLE-POLYGON test (no non-adjacent edges cross, the correctness precondition for area and point-in-polygon) and pairwise intersection counting, verified on crossing, touching, collinear-overlap, parallel, and disjoint segments, that the intersection point is correct and lies on both segments, that a convex polygon is simple while a figure-eight is not, and that a 5x5 grid has exactly 25 crossings.
Line-segment intersection via the orientation predicate
case intersect? crossing point
proper X-crossing True (2.0, 2.0)
T-touch (endpoint on edge) True (2.0, 0.0)
shared endpoint True (2.0, 2.0)
collinear overlap True -- (none / not a single point)
parallel, apart False -- (none / not a single point)
disjoint False -- (none / not a single point)
Simple-polygon test (no non-adjacent edges cross -- the precondition for area/PIP):
square: simple = True
convex pentagon: simple = True
arrow (non-convex): simple = True
figure-eight (self-crossing): simple = False
A 4x6 grid of horizontal/vertical segments has 24 crossings (4*6).
orient(a,b,c) = sign of the cross product tells which side of line ab point c is on.
Two segments properly cross iff each straddles the other's line (opposite orientations
at its endpoints); zero orientations mean collinear/touching, settled by a bounding-box
check. No slopes, so vertical segments are no trouble -- the robust geometric predicate.
wrote C:\Users\acwic\symplectic-nbody\examples\output\segment_intersection.svgPoint-in-polygon: ray casting vs winding number
Testing whether a point lies inside a polygon is the workhorse of hit-testing, GIS, and rendering. For a convex polygon a side-of-each-edge test suffices, but general (concave, star-shaped) polygons need one of two classic methods. RAY CASTING shoots a ray to infinity and counts edge crossings -- odd is inside; simple and fast, but blind to winding, so it uses the even-odd rule. WINDING NUMBER sums the signed turns the polygon makes around the point; nonzero is inside, and it correctly handles self-intersecting polygons where parity would disagree. The delicate part is boundary and vertex-graze cases: this module uses a half-open edge convention so a ray grazing a vertex counts exactly once, plus an explicit on-boundary test, and also gives the signed area (orientation) and centroid. Verified that both methods agree on convex, concave, and star polygons and across a grid, that boundary points are detected, that the signed area's sign tracks vertex orientation, and -- the textbook case -- that on a PENTAGRAM's doubly-wound centre the two rules DISAGREE: even-odd says outside, nonzero-winding says inside (winding count 2).
Point-in-polygon: ray casting vs winding number
Concave arrow polygon, area 12.0, centroid (2.67, 3.00), orientation CCW
Over a 7x7 grid: 12/49 points inside; ray casting and winding
number agree on all of them (0 disagreements) -- as they must for a
simple polygon.
Self-intersecting PENTAGRAM (a 5-point star drawn in one stroke):
its centre pentagon is enclosed by TWO loops, so the two rules disagree --
ray casting (even-odd): centre is OUTSIDE
winding number (nonzero): centre is INSIDE
winding count at centre: 2 (wrapped twice)
Ray casting counts edge crossings (odd = in); winding sums the signed turns of the
polygon around the point (nonzero = in). They agree on any simple polygon; on a
self-overlapping one, even-odd cancels a doubly-wrapped region to 'out' while winding
keeps it 'in'. Both handle concavity that a convex side-test cannot.
wrote C:\Users\acwic\symplectic-nbody\examples\output\point_in_polygon.svgPolygon clipping: intersecting a polygon with a window
Clipping a polygon to a rectangular viewport -- or any convex region -- is fundamental to rendering (nothing off-screen is drawn), CAD, and GIS overlay. The Sutherland-Hodgman algorithm clips the SUBJECT polygon against each edge of the CONVEX CLIP polygon in turn, feeding the output of one edge as input to the next; whatever survives all clip edges is exactly the intersection. Each single-edge clip is a linear scan: for every subject edge s->e, keep e if it is inside, add the crossing point when the edge leaves or enters, drop it when both ends are outside -- 'inside' decided by the same cross-product orientation predicate as everywhere in geometry. O(n*k) for an n-vertex subject and k-edge window; the clip must be convex but the subject may be concave. This module implements clipping against an arbitrary convex clip polygon plus a rectangle-window convenience, verified that a polygon fully inside is unchanged, one fully outside clips to empty, two overlapping squares clip to their analytic 2x2 overlap, a square clips to a triangular or diamond window at the right area, a concave subject stays within the window bounds, and the clipped area never exceeds the original.
Sutherland-Hodgman polygon clipping: subject INTERSECT convex window
subject polygon: 7 vertices, area 37.00
clip to rectangle (2, 2, 8, 8): 13 vertices, area 30.25
retained 82% of the subject's area; clipped area <= original: True
Clipping the same subject to different convex windows:
rectangle [2,8]^2: clipped area 30.25 (13 vertices)
triangle: clipped area 29.85 (9 vertices)
diamond: clipped area 30.10 (11 vertices)
tiny centre box: clipped area 4.00 (4 vertices)
Edge cases:
fully inside the window -> unchanged (area 4.0)
fully outside -> empty ([])
The algorithm clips the subject against each clip edge in turn, feeding the output
of one edge into the next. Per edge it keeps vertices on the inside and adds the
crossing point wherever an edge exits or enters -- O(n*k) for an n-gon and k-edge
convex window. This is how a renderer discards geometry outside the viewport.
wrote C:\Users\acwic\symplectic-nbody\examples\output\polygon_clip.svgMarching squares: contour lines from a scalar field
Given a 2-D grid of scalar values -- a height map, a temperature field, a signed-distance function -- marching squares finds the ISO-CONTOUR at a chosen level: the curve where the field equals it (a topographic contour, an isotherm, a blob boundary). It is the 2-D sibling of marching cubes (the isosurface method behind medical imaging) and the workhorse behind every contour plot. The idea is local: at each grid cell the four corners are above or below the level, giving a 4-bit CASE INDEX (16 possibilities) that says which cell edges the contour crosses, and LINEAR INTERPOLATION between corner values places each crossing exactly where the field equals the level -- so the curve is smooth, not blocky. The two ambiguous saddle cases (opposite corners high) are resolved consistently by the cell-center average. This module builds the 16-case lookup, extracts contour segments from a grid or a sampled function, and sums contour length, verified that a radial field contours to circles of the right radius and circumference, a linear ramp gives straight contours, a level outside the field range yields nothing, a diagonal saddle gives two segments, and a closed blob's contour forms closed loops with no loose ends.
Marching squares: iso-contours of a scalar field
Radial field f = x^2 + y^2 -- contours should be circles:
level radius segments length 2*pi*r
1 1.00 96 6.278 6.283
4 2.00 192 12.564 12.566
9 3.00 288 18.848 18.850
16 4.00 384 25.131 25.133
Gaussian terrain -- contour-line count at several heights:
height -0.5: 98 contour segments, total length 7.85
height 0.0: 128 contour segments, total length 10.37
height 0.5: 330 contour segments, total length 26.34
height 1.0: 278 contour segments, total length 22.18
height 2.0: 100 contour segments, total length 8.00
Each grid cell's four corners are above or below the level -> a 4-bit case index
selecting which edges the contour crosses; linear interpolation places the crossing
exactly where the field equals the level, so the curve is smooth. Two ambiguous saddle
cases are resolved by the cell-center average. This is how every contour plot is drawn.
wrote C:\Users\acwic\symplectic-nbody\examples\output\marching_squares.svgBresenham rasterization: lines and circles from integer math
Turning a mathematical line into pixels sounds trivial -- step x, round y -- but that needs slow floating point per pixel and mis-steps on steep lines. Bresenham's algorithm draws a line using ONLY INTEGER additions and comparisons: it tracks an error term measuring how far the true line has drifted from the chosen pixel, and whenever that error crosses a threshold it steps the minor axis and corrects. No division, no floating point, no rounding -- which is why it ran on the first plotters and still underlies GPU line rasterizers and grid games. All eight octants are handled uniformly with absolute deltas and step signs; the companion MIDPOINT CIRCLE draws one octant and mirrors it into the other seven by symmetry. This module implements line drawing, the midpoint circle, and a filled disk, verified that a line hits both endpoints, is 8-connected, stays within half a pixel of the true line, is symmetric under reversal, and handles every octant; and that a circle's pixels all lie within half a pixel of the true radius with 8-fold symmetry and a count tracking the circumference.
Bresenham rasterization: lines and circles from integer arithmetic only
Lines fanning from the bottom-left corner (integer pixels):
|# # # #|
|# # # ## |
|# # # ## |
|# # ## ## |
|# # # # |
|# # # ## |
|# # # ## |
|# # # ## ##|
|# # # ## #### |
|# # # # #### |
|## #### #### |
|##### #### |
|###### |
|########################|
Every rasterized line is connected and within half a pixel of the true line:
(0, 0)->(10, 3): 11 pixels, connected=True, max error 0.479
(0, 0)->(3, 10): 11 pixels, connected=True, max error 0.479
(0, 0)->(10, 10): 11 pixels, connected=True, max error 0.000
(0, 0)->(-8, 5): 9 pixels, connected=True, max error 0.424
Midpoint circle, radius 8:
| |
| ooooo |
| oo oo |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| o o |
| oo oo |
| ooooo |
| |
44 pixels, max deviation from the true radius: 0.384 (< 0.5, sub-pixel)
Circle pixel count tracks the circumference 2*pi*r (integer steps ~ r):
r = 5: 28 pixels (2*pi*r = 31)
r = 10: 56 pixels (2*pi*r = 63)
r = 20: 112 pixels (2*pi*r = 126)
r = 40: 228 pixels (2*pi*r = 251)
Bresenham tracks an integer error term: the signed distance the true line has
drifted from the current pixel. When it crosses a threshold, step the minor axis and
correct the error -- no floating point, no division, no rounding. The midpoint circle
does the same with a decision variable and draws one octant, mirroring it eight ways.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bresenham.svgFlood fill: paint bucket, region growing, connected components
Flood fill answers 'which cells are reachable from here through a region of the same value?' -- the paint-bucket tool, the region-grow of image segmentation, and the reachability query behind maze solving. From a seed it spreads to same-valued neighbours until it hits a boundary. Three strategies are here: a QUEUE/STACK fill (BFS/DFS visiting each cell once), a SCANLINE fill (painting whole horizontal runs and seeding only the rows above and below -- the classic optimization for big flat regions), and CONNECTED-COMPONENT LABELING (fill from every unlabeled cell to partition the grid into maximal same-valued regions). CONNECTIVITY is a parameter: 4-connected (orthogonal) or 8-connected (including diagonals), which changes what counts as one region. This module implements all three fills (4- and 8-connected) plus component labeling and sizing, verified that the strategies produce identical results, that fills respect barriers and the grid edge, that 8-connectivity merges diagonal regions 4-connectivity separates, that a bounded region leaves the rest untouched, and that a checkerboard has 9 components under 4-connectivity but 2 under 8.
Flood fill: paint bucket, region growing, connected components
Canvas (# = wall, . = empty):
........
.#####..
.#...#..
.#...##.
.###..#.
...####.
........
Paint-bucket from inside the wall (8 cells filled, ~ = paint):
........
.#####..
.#~~~#..
.#~~~##.
.###~~#.
...####.
........
The fill stays inside the wall -- the barrier is respected.
Filling the OUTER region by three strategies: queue 30, stack 30, scanline 30 cells -- all identical: True
Connected components of a value grid:
4-connectivity: 8 components, sizes [1, 2, 3, 3, 3, 3, 3, 7]
8-connectivity: 6 components (diagonal touches merge some)
A diagonal chain of 1s:
#..
.#.
..#
region size from a corner: 4-conn = 1 (isolated), 8-conn = 3 (all three joined)
Flood fill spreads from a seed to same-valued neighbours until it meets a boundary.
The scanline variant paints whole horizontal runs at once, seeding only the rows above
and below -- far fewer stack operations on big flat regions. Run it from every
unlabeled cell and you get connected-component labeling, the atom of image analysis.
wrote C:\Users\acwic\symplectic-nbody\examples\output\flood_fill.svgBezier curves: control-point curves via de Casteljau
A Bezier curve is defined not by points it passes through but by CONTROL POINTS that pull it into shape -- it starts at the first, ends at the last, and is tugged toward the ones between. That handles behaviour is why Bezier curves are the primitive of SVG and PDF paths, TrueType fonts, and animation easing. A degree-n curve blends its n+1 control points by the Bernstein polynomials, but the elegant, numerically stable way to evaluate it is DE CASTELJAU's algorithm: repeatedly take pairwise linear interpolations of the control points at parameter t until one point remains -- that is B(t). The same construction, kept rather than discarded, SPLITS the curve at t into two Bezier curves (subdivision, the basis of adaptive rendering). This module implements de Casteljau evaluation, the derivative (tangents), subdivision, degree elevation, and arc length by adaptive subdivision, verified that the curve hits its first and last control points, that de Casteljau matches the Bernstein sum, that a linear curve is exactly the straight segment, that the curve stays within the convex hull of its control points, that subdivision reproduces the original and degree elevation preserves the shape, and that a linear curve's arc length is the endpoint distance.
Bezier curves: shaped by control points, evaluated by de Casteljau
Quadratic Bezier, control points [(0, 0), (2, 4), (4, 0)]
B(0) = (0, 0) (first control point)
B(0.5) = (2.0, 2.0)
B(1) = (4, 0) (last control point)
arc length 5.9158, tangent at 0.5 = (4.0, 0.0)
Cubic Bezier, control points [(0, 0), (1, 4), (3, -2), (4, 2)]
arc length 5.9769
every sampled point inside the control hull: True
Subdivision at t=0.5 splits into two cubics that reproduce the curve: True
left control points: [(0, 0), (0.5, 2.0), (1.25, 1.5), (2.0, 1.0)]
right control points: [(2.0, 1.0), (2.75, 0.5), (3.5, 0.0), (4, 2)]
Degree elevation: the quadratic rewritten as a cubic (same shape: True)
new control points: [(0, 0), (1.33, 2.67), (2.67, 2.67), (4, 0)]
de Casteljau evaluates B(t) by repeated linear interpolation of the control points:
interpolate adjacent pairs at t, then interpolate the results, until one point remains
-- that point is on the curve. Keeping the triangle's outer edges splits the curve in
two. It is numerically stable and the basis of every vector-graphics renderer.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bezier.svgBurrows-Wheeler transform: the heart of bzip2
The Burrows-Wheeler transform is the clever core of bzip2. On its own it compresses NOTHING -- it is a reversible PERMUTATION -- but it rearranges bytes so characters sharing a context cluster into long runs that later stages squeeze. It sorts all rotations of a sentinel-terminated string and takes the last column; the inverse reconstructs the original by the LF-mapping. The bzip2 pipeline chains three reversible stages: BWT (cluster like-context bytes into runs), MOVE-TO-FRONT (recode each byte as its index in a running alphabet, so a run becomes a run of zeros), and RUN-LENGTH encoding (collapse those runs into value/count pairs). This module implements BWT and its inverse (with a sentinel so any input works), move-to-front, run-length coding, and the full forward/backward pipeline, verified that BWT round-trips any string, that it genuinely increases the mean run length on structured text (DNA-like input becomes ~15x runnier), that MTF and RLE round-trip, that the whole pipeline is lossless across 50 random strings, and that it yields far fewer tokens than the input length on repetitive data.
Burrows-Wheeler transform + the bzip2-style pipeline
BWT rearranges text so characters sharing a context cluster into runs:
banana -> BWT 'annb$aa' (reversible: True)
mississippi -> BWT 'ipssm$pissii' (reversible: True)
abracadabra -> BWT 'ard$rcaaaabb' (reversible: True)
On structured text the mean run length jumps (more compressible):
repeated phrase: mean run 1.00 -> 6.11 (6.1x runnier)
DNA-like: mean run 1.00 -> 14.60 (14.6x runnier)
English-ish: mean run 1.06 -> 4.60 (4.4x runnier)
Full pipeline (BWT -> move-to-front -> run-length) on repetitive input:
input 100 chars -> 5 RLE pairs (20.0x fewer tokens), lossless: True
input 120 chars -> 11 RLE pairs (10.9x fewer tokens), lossless: True
input 120 chars -> 33 RLE pairs (3.6x fewer tokens), lossless: True
Worked example on 'banana':
BWT: 'annb$aa'
move-to-front: [1, 3, 0, 3, 3, 3, 0]
run-length: [(1, 1), (3, 1), (0, 1), (3, 3), (0, 1)]
decode: 'banana'
BWT alone compresses nothing -- it is a reversible permutation -- but it makes the
data 'runnier' by clustering same-context bytes. Move-to-front then turns runs into
small numbers (mostly zeros), and run-length collapses those into (value, count) pairs.
Every stage inverts exactly, so the pipeline is lossless: this is bzip2's core.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bwt.svgArithmetic coding: entropy compression past the Huffman limit
Huffman coding assigns each symbol a whole number of bits, so it wastes up to nearly a bit per symbol -- disastrous when one symbol has probability 0.9 and 'deserves' 0.15 bits. ARITHMETIC CODING escapes that: it encodes the ENTIRE message as a single number in [0, 1), progressively narrowing an interval by each symbol's probability, so a symbol costing 0.15 bits really adds only 0.15 bits. It gets within a fraction of a bit of the Shannon entropy regardless of the distribution -- which is why it sits inside JPEG and H.264. Naively it needs unbounded-precision reals; the practical trick is integer RANGE CODING with RENORMALIZATION, emitting settled top bits and shifting, with an underflow counter for the straddle-the-middle case. This module implements an integer arithmetic coder and decoder driven by a frequency model, verified that encode/decode round-trips arbitrary messages (60 random ones included), that the code length approaches the entropy (within a couple of bits over the whole message), that it beats Huffman's one-bit-per-symbol floor on skewed data (~49% smaller at 90% skew), and that it handles single-symbol and uniform alphabets.
Arithmetic coding vs Huffman vs entropy
Arithmetic coding encodes the whole message as one number, so it is not limited to
whole-bit codewords -- it hugs the entropy where Huffman must round up.
distribution entropy arithmetic Huffman AC saves
very skewed 0.557 0.560 1.100 49.1%
skewed 1.353 1.370 1.400 2.1%
mild skew 1.559 1.570 1.600 1.9%
near uniform 1.585 1.600 1.660 3.6%
uniform-4 2.000 2.017 2.000 -0.8%
English-ish 4.339 4.348 4.409 1.4%
The bigger the skew, the more Huffman's whole-bit codewords waste and the more
arithmetic coding wins: at 90% one symbol, entropy is ~0.56 bits but Huffman is
forced to spend at least 1 bit on the rarer symbols. All encodings round-trip exactly.
How it works: start with the interval [0,1); each symbol narrows it to its
probability-weighted sub-interval; after the whole message, any number in the final
tiny interval names the message. Integer range coding with renormalization keeps it
exact without unbounded-precision reals -- the entropy coder inside JPEG and H.264.
wrote C:\Users\acwic\symplectic-nbody\examples\output\arithmetic_coding.svgSequence alignment: global and local dynamic programming
Aligning two sequences -- lining them up to maximize matching symbols by inserting gaps -- is the central operation of bioinformatics, spell-check, and diff tools. Where edit distance just counts operations, alignment produces the actual correspondence and a SCORE weighting matches, mismatches, and gaps. Both algorithms fill a DP matrix where cell (i,j) is the best score aligning the first i and j symbols via three moves (align, gap in a, gap in b). NEEDLEMAN-WUNSCH aligns the sequences end to end (global): the borders are cumulative gap penalties and the answer is the corner cell. SMITH-WATERMAN finds the best-matching SUBSEQUENCES (local): scores are floored at zero so a bad stretch resets, and the answer traces back from the maximum cell -- ideal for a conserved motif inside dissimilar sequences. Traceback reconstructs the two gapped strings. This module implements both with configurable match/mismatch/gap scoring, verified that identical sequences align perfectly, that the global score matches recomputing it from the alignment, that a local alignment isolates an embedded motif the global one drags flanks into, that the gap penalty steers whether gaps or mismatches are used, and that the score is symmetric.
Sequence alignment: global (Needleman-Wunsch) vs local (Smith-Waterman)
Global alignment of two DNA-like sequences (aligned end to end):
GCA-TGCU
| | |.|.
G-ATTACA
score 0, identity 67% (| match, . mismatch, space gap)
Global alignment of two words (KITTEN vs SITTING):
KITTEN-
.|||.|
SITTING
score 1
A shared motif GATTACA buried in different flanks:
seq A = GGGGGATTACATTTTT
seq B = CCGATTACAAA
GLOBAL alignment drags in the mismatched flanks:
GGGGGATTACATTTTT
..||||||| ..
--CCGATTACA---AA
global score -2
LOCAL alignment isolates the conserved motif:
GATTACA
|||||||
GATTACA
local score 14, identity 100%
Gap penalty steers the alignment (AAAA vs ATAA):
gap=-1, mismatch=-10: A-AAA / AT-AA (cheap gaps -> insert a gap)
gap=-10, mismatch=-1: AAAA / ATAA (costly gaps -> keep the mismatch)
Both fill a DP matrix where cell (i,j) is the best score aligning the first i and j
symbols via align / gap-in-a / gap-in-b. Needleman-Wunsch seeds the borders with
cumulative gap penalties and reads the corner (global); Smith-Waterman floors scores
at zero and starts from the max cell (local). Traceback reconstructs the gapped strings.
wrote C:\Users\acwic\symplectic-nbody\examples\output\sequence_alignment.svgLinear-time string matching: KMP, Z-algorithm, Manacher
Finding a pattern in a text is the most basic string operation, and the naive try-every-position approach is O(n*m). Three classics do fundamental string tasks in LINEAR time by precomputing self-overlap so the search never re-examines a character. KMP builds the PREFIX FUNCTION -- for each pattern position, the longest proper prefix that is also a suffix -- so a mismatch shifts the pattern by more than one without rescanning the text. The Z-ALGORITHM computes, for each position, the longest substring there matching a prefix of the whole string; running it on pattern + separator + text turns matching into reading off Z-values. MANACHER finds the longest PALINDROMIC substring in O(n) by reusing mirror information where the naive approach is O(n^2). This module implements the prefix function and KMP search, the Z-array and Z-based search, and Manacher's palindrome, verified that KMP and the Z-search find exactly the same (overlapping) occurrences as a brute-force scan across 400 random strings, that the prefix function matches its definition, and that Manacher's longest palindrome matches brute force in length across 100 strings and finds the known cases (racecar, geeksskeeg).
Linear-time string matching: KMP, Z-algorithm, Manacher
KMP prefix function of 'ababaca':
index: 0 1 2 3 4 5 6
char: a b a b a c a
pi: 0 0 1 2 3 0 1
(pi[i] = longest proper prefix that is also a suffix of the first i+1 chars --
on a mismatch the pattern shifts by more than one without rescanning the text)
Searching for 'ababaca' in 'abababacabababacaba':
KMP occurrences: [2, 10]
Z-algorithm: [2, 10]
brute-force (check): [2, 10]
all three agree: True
Overlapping matches -- 'aa' in 'aaaaa': [0, 1, 2, 3] (4 occurrences)
Z-array of 'aabxaabxcaabxaab' (z[i] = match length with the prefix):
0 1 0 0 4 1 0 0 0 7 1 0 0 3 1 0
Longest palindromic substring (Manacher, O(n)):
babad -> 'bab'
forgeeksskeegfor -> 'geeksskeeg'
abacdfgdcaba -> 'aba'
racecar -> 'racecar'
banana -> 'anana'
All three exploit precomputed self-overlap to skip redundant comparisons: KMP's
prefix function, Z's prefix-match array, and Manacher's mirror reuse. Naive matching
is O(n*m) and naive palindrome search O(n^2); these are all O(n) -- the difference
between scanning a genome once and scanning it thousands of times.
wrote C:\Users\acwic\symplectic-nbody\examples\output\string_matching.svgSuffix arrays: a compact string index
A suffix array is the sorted order of all SUFFIXES of a string, stored as their start indices -- packing almost everything a suffix TREE offers (fast substring search, longest-repeated-substring, longest-common-substring) into a single length-n integer array, which is why it underlies full-text search, bioinformatics indices, and the bzip2 family. Once built, 'does pattern P occur?' is a BINARY SEARCH over the sorted suffixes in O(m log n), and every occurrence is a contiguous run. It is built here by PREFIX DOUBLING (sort by the first 1, 2, 4, ... characters using previous ranks as keys, O(n log^2 n)), and the companion LCP ARRAY (longest common prefix of adjacent sorted suffixes) is built in O(n) by KASAI's algorithm -- the largest LCP value is the longest repeated substring. This module builds the suffix array, the LCP array, substring search, longest repeated substring, and longest common substring, verified that the suffix array is the true sorted order (checked against a brute-force sort over 150 strings), that search finds exactly the same occurrences as a scan, that the LCP array matches the direct prefix computation, and that the longest repeated and common substrings match brute force.
Suffix array: the sorted order of all suffixes, as an integer index
string: 'mississippi'
rank SA LCP suffix
0 10 0 i
1 7 1 ippi
2 4 1 issippi
3 1 4 ississippi
4 0 0 mississippi
5 9 0 pi
6 8 1 ppi
7 6 0 sippi
8 3 2 sissippi
9 5 1 ssippi
10 2 3 ssissippi
Substring search is a binary search over the sorted suffixes (O(m log n)):
'issi': occurs at [1, 4]
'ss': occurs at [2, 5]
'ppi': occurs at [8]
'xyz': not found
Longest repeated substring (the largest LCP value): 'issi'
Longest repeated substring of a few strings:
'banana' -> 'ana'
'abracadabra' -> 'abra'
'the theme of these theses' -> ' these'
'aaaa' -> 'aaa'
Longest common substring of two strings (via a combined suffix array):
'dogandcat' & 'thecatsat' -> 'cat'
'bioinformatics' & 'informant' -> 'informa'
'GATTACAGG' & 'TTGATTACA' -> 'GATTACA'
The suffix array packs a suffix tree's power into one length-n array: sorted
suffixes make substring search a binary search, every occurrence is a contiguous
run, and the LCP array turns tree queries into array scans -- the largest LCP is the
longest repeated substring. Built by prefix doubling; LCP by Kasai in linear time.
wrote C:\Users\acwic\symplectic-nbody\examples\output\suffix_array.svgQuadtrees: recursive spatial partition for region queries
A quadtree indexes 2-D points by recursively splitting a square region into four QUADRANTS whenever a node exceeds a small CAPACITY, so empty regions stay shallow and crowded regions subdivide deeply -- the tree adapts to the data's density. It is the 2-D workhorse for spatial queries (which points are in this rectangle?), collision broad-phase, image compression, and the Barnes-Hut n-body approximation, answering a range query by visiting only the nodes whose square OVERLAPS the query and pruning whole branches that fall outside. Where a k-d tree splits alternately on one coordinate (a binary tree), a quadtree splits on both at once (a 4-way tree tied to axis-aligned squares), making rectangle and circle range queries especially natural. This module implements a point-region quadtree with insert (and a max-depth guard so coincident points do not subdivide forever), rectangle and circular range queries, and nearest-neighbour, verified that rectangle and radius queries return exactly the same points as a linear scan across 80 random queries, that nearest matches the brute-force nearest, that out-of-bounds points are rejected, that a dense cluster subdivides deeply while spread data stays shallow, and that duplicate points are all stored.
Quadtree: recursive 2-D spatial partition for region queries 180 points over a 100x100 area, node capacity 4 tree depth 6 (denser regions subdivide deeper) Rectangle query [30,20]-[70,60]: 24 points (matches brute force: True) Circle query centre (22,82) r=12 (in the dense cluster): 66 points (matches brute force: True) Nearest point to (50,50): (55.5, 47.3) (matches brute force: True) A quadtree splits a square into four quadrants whenever a node exceeds its capacity, so empty regions stay shallow and crowded ones subdivide deeply. A range query visits only the cells whose square overlaps the query, pruning whole branches -- the basis of collision broad-phase and the Barnes-Hut n-body approximation. wrote C:\Users\acwic\symplectic-nbody\examples\output\quadtree.svg
Maximum flow: augmenting paths, the min-cut theorem, and matching
How much can flow from a SOURCE to a SINK through a network of capacitated pipes? Ford-Fulkerson repeatedly finds an AUGMENTING PATH with spare capacity and pushes flow along it, maintaining a RESIDUAL graph where each used edge gains a reverse edge so later paths can cancel and reroute earlier flow -- the trick that lets a greedy-looking method reach the true optimum. Edmonds-Karp always takes the SHORTEST augmenting path (by BFS), guaranteeing O(V E^2) termination independent of the capacities. The celebrated MAX-FLOW MIN-CUT THEOREM says the maximum flow equals the minimum CUT -- the smallest total capacity of edges whose removal severs source from sink -- and after the flow saturates, the vertices still reachable from the source in the residual graph reveal exactly those bottleneck edges. A classic reduction turns BIPARTITE MATCHING into flow: a super-source into every left vertex, a super-sink from every right vertex, all capacities one, and the max flow is the matching size. This module implements Edmonds-Karp, min-cut extraction, and bipartite matching, verified that it hits the textbook flow of 23, that the min-cut capacity equals the flow, that flow conservation and capacity constraints hold, that residual rerouting achieves the true optimum on the anti-greedy network, and that matching-by-flow equals a direct augmenting-path matching over 40 random graphs.
Maximum flow: Edmonds-Karp, min-cut, and bipartite matching
A 6-node network (source 0, sink 5):
maximum flow: 23
minimum cut edges: [(1, 3), (4, 3), (4, 5)]
min-cut capacity: 23 == max flow: True
(the max-flow min-cut theorem: the bottleneck edges limit the whole network)
Small networks:
two parallel pipes (5, 3): 8
series with a bottleneck (10, 3): 3
anti-greedy (needs rerouting): 2 (residual reverse edges reroute flow)
Bipartite matching by reduction to max flow (jobs to workers):
8 allowed assignments -> maximum matching of 4:
Ann -> cook
Bob -> clean
Cy -> drive
Dot -> shop
Ford-Fulkerson pushes flow along augmenting paths and updates a residual graph where
each used edge gains a reverse edge (so later paths can cancel flow); Edmonds-Karp
always takes the shortest such path (BFS) for O(V E^2) time. When no augmenting path
remains, the source-reachable residual set defines the minimum cut = the maximum flow.
wrote C:\Users\acwic\symplectic-nbody\examples\output\max_flow.svgDe Bruijn sequences: every window exactly once, via Eulerian circuits
A DE BRUIJN SEQUENCE B(k, n) is a cyclic string over a k-symbol alphabet in which every one of the k**n possible length-n strings appears EXACTLY ONCE as a wrapped substring -- a single string of length k**n that packs all k**n windows with zero waste. They are the mathematics behind absolute rotary shaft encoders (read n adjacent tracks to know the angle), the shortest string that brute-forces every PIN, card tricks, and de novo genome assembly from overlapping k-mers. The construction is a gem: build the DE BRUIJN GRAPH whose vertices are the k**(n-1) shorter strings and whose edges are the k**n windows, then find an EULERIAN CIRCUIT -- a closed walk using every edge once. Because every vertex has equal in- and out-degree, one always exists, and HIERHOLZER'S ALGORITHM finds it in linear time by splicing detour cycles wherever unused edges remain. Reading the appended symbol along the circuit yields the sequence. This module builds B(k, n) for any parameters, offers the greedy prefer-largest 'Ford' construction, and exposes a general Eulerian path/circuit finder for arbitrary directed multigraphs, verified that every window appears exactly once (a bijection onto all k**n strings), that lengths are exactly k**n, that B(2,3) matches the classic example, that the greedy sequence is also valid, and that the Eulerian finder recovers a circuit using each edge once and rejects graphs where none exists.
De Bruijn sequences: every length-n window exactly once
B(2,3) = 01011100 (length 8 = 2^3)
its 8 cyclic windows (each binary triple exactly once):
010
101
011
111
110
100
000
001
is a valid De Bruijn sequence: True
B(2,4) = 0100110101111000 (length 16 = 2^4)
all 16 nibbles appear once: True
B(10,4): the shortest cyclic string containing all 10000 four-digit PINs
length 10000 (vs 40000 keypresses to type each PIN separately)
first 40 digits: 0100020003000400050006000700080009001100...
every PIN appears exactly once: True
B(2,6): a 64-position absolute rotary encoder track
reading any 6 adjacent bits gives a unique angle: True
Each sequence is an Eulerian circuit of the De Bruijn graph: vertices are the
(n-1)-length strings, edges are the n-length strings, and every vertex has equal in-
and out-degree, so Hierholzer's cycle-splicing finds a walk using every edge once.
wrote C:\Users\acwic\symplectic-nbody\examples\output\de_bruijn.svgRotating calipers: diameter, width, and the minimum-area bounding box
Once a point set's CONVEX HULL is known, a whole family of extremal measurements can be read off in a single sweep instead of the naive all-pairs comparison. The ROTATING CALIPERS technique imagines parallel lines pinching the polygon and rotating in lockstep around it; the vertices they touch enumerate exactly the ANTIPODAL PAIRS -- pairs with parallel supporting lines -- and since the farthest pair (the DIAMETER) is always antipodal, one rotation finds it in O(h). The same structure yields the WIDTH (the thinnest slab of parallel lines that still contains everything) and, by the Freeman-Shapira theorem, the MINIMUM-AREA enclosing RECTANGLE, which must have one side flush with a hull edge -- so trying each edge orientation and measuring the extent parallel and perpendicular to it gives the optimum. These power collision bounding volumes, shape metrology, and part orientation for packing and machining. This module computes the hull (monotone chain), the diameter, the width, and the minimum-area (and -perimeter) rectangle with its corners, verified against brute force: the calipers diameter equals the O(n^2) farthest pair, the width equals the brute minimum over hull directions, and the minimum rectangle contains every point and never beats the axis-aligned box -- across many random and structured sets, with exact values on squares and triangles.
Rotating calipers: diameter, width, and minimum-area bounding box
40 points, hull has 8 vertices
diameter (farthest pair): 277.31
between (46.4, 58.3) and (276.9, 212.4)
width (thinnest slab): 66.70
minimum-area bounding box:
area 18454.9, 276.7 x 66.7, perimeter 686.7
axis-aligned box area 37895.0 -> the rotated box saves 51.3%
A single rotation of the calipers enumerates every antipodal pair (the diameter is
always among them), the thinnest slab gives the width, and the minimum-area rectangle
must have a side flush with a hull edge -- so all three fall out of one hull sweep.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rotating_calipers.svgTernary search trees: a trie's prefix power at a BST's space
A TERNARY SEARCH TREE gives every node one character and THREE children: LEFT for characters that sort before it, RIGHT for those after, and a MIDDLE child that advances to the next character of the key. Following the middle links spells out a key just like a trie, but the alternatives at each position live in a little binary search tree rather than a k-way array -- so a TST keeps a trie's prefix structure and sorted traversal while spending BST-like space, with no wasted arrays for large alphabets and graceful behaviour on sparse key sets. Championed by Bentley and Sedgewick, they are a classic backing store for spell-checkers, autocomplete, and prefix routing. A single three-way comparison drives every operation: insert and lookup walk left/right on the comparison and drop into the middle child on a match; PREFIX COMPLETION enumerates the subtree below a prefix in sorted order; and -- the trick hash maps cannot do cheaply -- PARTIAL-MATCH search with '.' wildcards recurses into all three children at a wildcard position, so 'c.t' finds cat, cot, cut in one pass. This module implements insert with values, lookup, deletion, sorted iteration, autocomplete, longest-prefix-of, and wildcard search, verified against a plain dict and brute force: exact keys and values, truly sorted iteration, prefix completion matching a startswith scan, wildcard search matching a regex scan over hundreds of random queries, and dict-like deletion.
Ternary search tree: trie prefix power with BST space
inserted 12 words: cat, cats, car, card, care, dog, do, dodge, dot, day, ape, app
stored (sorted): ape, app, car, card, care, cat, cats, day, do, dodge, dog, dot
Autocomplete (keys_with_prefix):
'ca' -> ['car', 'card', 'care', 'cat', 'cats']
'do' -> ['do', 'dodge', 'dog', 'dot']
'app' -> ['app']
'd' -> ['day', 'do', 'dodge', 'dog', 'dot']
Longest prefix of a query (longest_prefix_of):
'cards' -> 'card'
'doghouse' -> 'dog'
'dotted' -> 'dot'
'apex' -> 'ape'
Wildcard search ('.' matches any one letter):
'c.r' -> ['car']
'do.' -> ['dog', 'dot']
'ca..' -> ['card', 'care', 'cats']
'..t' -> ['cat', 'dot']
Each node holds one character and three children: left/right for the BST of
alternatives at this position, and a middle link that advances to the next character
(spelling a key, like a trie). One three-way comparison drives every operation.
wrote C:\Users\acwic\symplectic-nbody\examples\output\ternary_search_tree.svgDelaunay triangulation and the Voronoi diagram: two faces of proximity
Given a scatter of points, two dual structures capture who is near what. The VORONOI DIAGRAM cuts the plane into one cell per site -- the region closer to that site than to any other -- so a single point location answers a nearest-neighbour query. Its straight-line dual is the DELAUNAY TRIANGULATION, connecting two sites whenever their Voronoi cells touch. The Delaunay triangulation is the 'roundest' triangulation: it maximizes the minimum angle (avoiding slivers) and is characterized by the EMPTY-CIRCUMCIRCLE property -- the circle through any triangle's three vertices contains no other site. Both are workhorses of mesh generation, terrain modelling, interpolation, and spatial statistics. The construction here is BOWYER-WATSON: enclose the sites in a super-triangle, then insert points one by one, deleting every triangle whose circumcircle contains the new point and re-triangulating the star-shaped cavity that opens up. The in-circle test is evaluated in EXACT rational arithmetic so adjacent triangles never disagree and the mesh stays a valid triangulation. The Voronoi diagram falls out for free -- its vertices are the triangles' circumcentres. Verified that every output triangle has an empty circumcircle against all sites, that the triangle count obeys Euler's 2n-2-h, that the triangle areas exactly fill the convex hull (no gaps or overlaps), that Voronoi vertices are equidistant from their three sites, and that each site's nearest neighbour is always a Delaunay edge.
Delaunay triangulation and the dual Voronoi diagram 24 sites Delaunay triangles: 37 (Euler: 2n-2-h) Voronoi edges (finite): 51 empty-circumcircle property holds: True average Delaunay degree: 5.00 every site's nearest neighbour is a Delaunay edge: True The Voronoi diagram partitions the plane into 'nearest-site' cells; its straight-line dual is the Delaunay triangulation, whose triangles have empty circumcircles and maximize the minimum angle. Voronoi vertices are the triangle circumcentres. wrote C:\Users\acwic\symplectic-nbody\examples\output\delaunay.svg
The simplex method: linear programming by walking polytope vertices
LINEAR PROGRAMMING maximizes or minimizes a linear objective under linear constraints -- the backbone of operations research, from diet and blending problems to scheduling, network flows, and the LP relaxations inside integer-programming solvers. The feasible region is a convex POLYTOPE, and a fundamental theorem guarantees that if an optimum exists it is attained at a VERTEX. The SIMPLEX METHOD, Dantzig's 1947 invention, starts at a vertex and repeatedly slides along an edge to an adjacent vertex that improves the objective, stopping when no improving edge remains -- at which point the vertex is provably optimal. It works on a TABLEAU: slack variables turn inequalities into equalities, and each PIVOT picks an entering variable by reduced cost and a leaving one by the minimum-ratio test, then does Gaussian elimination. BLAND'S RULE (smallest index) prevents cycling on degenerate problems, and a TWO-PHASE approach with artificial variables finds a starting vertex for >= and = constraints. This module solves LPs in general form (mixed <=, >=, = constraints, maximize or minimize) and reports optimal, unbounded, or infeasible, verified against hand-solved textbook LPs, against a brute-force solver that enumerates every basic vertex (60 random LPs), against the LP-duality theorem (primal optimum equals dual optimum), and on degenerate, unbounded, and infeasible instances.
The simplex method: linear programming by walking polytope vertices
Factory problem: maximize profit 3x + 5y
subject to x <= 4, 2y <= 12, 3x + 2y <= 18, x,y >= 0
-> status optimal, optimal profit 36.0 at x = 2.00, y = 6.00
Dual problem (minimize resource prices b.y): optimum 36.0
LP duality: primal 36.0 == dual 36.0 -> True
Diet problem: minimize cost 2x + 3y s.t. x+y>=10, x+3y>=18
-> minimum cost 24.0 at x = 6.00, y = 4.00
Detects unbounded LPs: unbounded
Detects infeasible LPs: infeasible
Simplex starts at a vertex of the feasible polytope and slides to an adjacent vertex
that improves the objective, until none does -- then that vertex is provably optimal.
Bland's rule (smallest-index pivot) prevents cycling on degenerate problems.
wrote C:\Users\acwic\symplectic-nbody\examples\output\simplex.svgMinHash and LSH: estimating set similarity at scale
How similar are two sets? The JACCARD SIMILARITY -- intersection over union -- is the natural measure, but comparing every pair among millions of sets is hopeless if each comparison touches the full sets. MINHASH, Broder's trick for AltaVista's near-duplicate web-page detection, compresses each set into a short SIGNATURE of k numbers such that the probability two signatures agree in any position equals exactly the Jaccard similarity of the sets -- so the fraction of matching positions is an unbiased estimate computed from k small integers instead of the whole sets. The mechanism is pure probability: under a random permutation of the universe, a set's minimum element is equally likely to be any of its members, so two sets share that minimum precisely when the overall minimum lies in their intersection, an event of probability |A n B| / |A u B|. Using k hash functions as permutations gives a signature whose error shrinks like 1/sqrt(k). To actually FIND similar pairs without comparing all pairs, LOCALITY-SENSITIVE HASHING splits each signature into bands and hashes each band; items that collide in any band become candidates, and tuning the bands shapes an S-curve that makes near-duplicates collide while keeping dissimilar pairs apart. This module implements MinHash signatures with universal hashing, the Jaccard estimator, and banded LSH, verified that the estimate converges to the true Jaccard as k grows (error tracking 1/sqrt(k)), that identical sets estimate 1 and disjoint ~0, and that LSH recalls every high-Jaccard pair while filtering dissimilar ones.
MinHash + LSH: estimating set similarity at scale
Documents (as character 3-shingle sets):
A: "the quick brown fox jumps over the lazy dog"
B: "the quick brown fox jumped over a lazy dog"
C: "the quick brown fox jumps over the lazy dog!"
D: "a completely different sentence about cats"
E: "cats are completely different from that sentence"
Pairwise similarity (MinHash estimate vs true Jaccard, k=200):
A-B: estimate 0.740 true 0.717 (err 0.023)
A-C: estimate 0.980 true 0.975 (err 0.005)
A-D: estimate 0.005 true 0.013 (err 0.008)
A-E: estimate 0.020 true 0.024 (err 0.004)
B-C: estimate 0.730 true 0.702 (err 0.028)
B-D: estimate 0.000 true 0.013 (err 0.013)
B-E: estimate 0.000 true 0.012 (err 0.012)
C-D: estimate 0.005 true 0.013 (err 0.008)
C-E: estimate 0.020 true 0.024 (err 0.004)
D-E: estimate 0.530 true 0.527 (err 0.003)
Estimate error shrinks like 1/sqrt(k):
k= 8 mean error 0.1301
k= 16 mean error 0.1023
k= 32 mean error 0.0701
k= 64 mean error 0.0477
k= 128 mean error 0.0341
k= 256 mean error 0.0245
k= 512 mean error 0.0179
LSH near-duplicate search (bands=50, rows=4, k=200):
candidate near-duplicate pairs: [('A', 'B'), ('A', 'C'), ('B', 'C'), ('D', 'E')]
S-curve 50% threshold at Jaccard ~ 0.376
Two signatures agree in a position with probability equal to the Jaccard similarity,
so matching-position fraction is an unbiased estimate. LSH bands make similar items
collide in a hash table, turning all-pairs search into a few lookups.
wrote C:\Users\acwic\symplectic-nbody\examples\output\minhash.svgCMA-ES: covariance matrix adaptation for black-box optimization
For minimizing a function with no gradient -- possibly noisy, non-convex, or badly scaled -- the COVARIANCE MATRIX ADAPTATION EVOLUTION STRATEGY is among the most powerful derivative-free optimizers known, the de-facto standard for hard continuous problems. It samples candidate solutions from a MULTIVARIATE NORMAL distribution and, generation by generation, reshapes that distribution toward better regions by adapting three things: the MEAN (moved to the weighted average of the best samples), the STEP SIZE (grown or shrunk by comparing the length of the path actually taken to the length expected under pure randomness), and the full COVARIANCE MATRIX (bent to align with the directions of recent progress, so the search ellipsoid learns the local curvature -- much like a second-order method learns the inverse Hessian, but with no derivatives). That self-adaptation is what lets it crack badly-conditioned and rotated problems that defeat coordinate-wise methods. This module implements a faithful (mu/mu_w, lambda)-CMA-ES with the standard strategy parameters and a self-contained Jacobi eigensolver for the covariance decomposition, verified that it converges to the global optimum of the sphere, Rosenbrock, ill-conditioned ellipsoid (condition 1e6), and shifted problems to near machine precision, that it beats random search by many orders of magnitude under an equal budget, that it handles a rotated anisotropic bowl (rotation invariance), and that it is fully reproducible from a seed.
CMA-ES: covariance matrix adaptation for black-box minimization sphere (4D) : fx = 6.376e-13 in 952 evals, x ~ [-0.000, -0.000, 0.000...] Rosenbrock (2D banana) : fx = 7.890e-13 in 870 evals, x ~ [1.000, 1.000] Rastrigin (2D, multimodal) : fx = 9.950e-01 in 12000 evals, x ~ [-0.995, -0.000] ellipsoid (3D, cond 1e6) : fx = 5.062e-13 in 1540 evals, x ~ [-0.000, -0.000, -0.000] On the sphere, 952 evaluations of random search reach only 2.687e+00, while CMA-ES reaches 6.376e-13 -- a difference of many orders. CMA-ES samples from a Gaussian and, each generation, moves its mean to the best samples, adapts the step size from the length of its cumulative path, and bends the covariance toward recent progress -- learning the landscape's curvature without gradients. wrote C:\Users\acwic\symplectic-nbody\examples\output\cma_es.svg
L-BFGS: limited-memory quasi-Newton optimization
When a function is smooth and its GRADIENT is available, QUASI-NEWTON methods are the fastest general-purpose minimizers. Newton's method rescales the gradient by the inverse HESSIAN to account for curvature and converge superlinearly, but forming an n x n Hessian costs O(n^2) memory and O(n^3) time. BFGS approximates the inverse Hessian from successive gradient differences with no second derivatives; L-BFGS ('limited memory') never stores the matrix at all -- it keeps only the last m pairs of (step, gradient-change) vectors and reconstructs the inverse-Hessian action on the gradient through the elegant TWO-LOOP RECURSION, so memory is O(m n) and each step is O(m n). It is the workhorse behind training logistic regression and conditional random fields, large-scale maximum likelihood, and countless scientific fits. A line search satisfying the Wolfe conditions picks a step that decreases the objective enough without overshooting, which also keeps the curvature pairs positive-definite. This module implements L-BFGS with the two-loop recursion, a Wolfe line search, and an automatic finite-difference gradient fallback, verified on the quadratic bowl, Rosenbrock (2D and 4D), a shifted optimum, and an ill-conditioned quadratic where it beats gradient descent by eighteen orders of magnitude, plus a logistic-regression fit that recovers the generating weights -- and its finite-difference gradients match the analytic ones.
L-BFGS: limited-memory quasi-Newton optimization
Rosenbrock (2D banana):
L-BFGS: fx = 2.745e-17 at (1.00000, 1.00000) in 34 iterations
Ill-conditioned quadratic (condition number 1000):
L-BFGS after 26 evals: 1.343e-18
gradient descent after 100 steps: 2.187e+00
-> L-BFGS uses curvature to rescale each direction; GD crawls along the stiff axis.
Logistic regression (300 points):
fitted weights [1.55, -1.77, 0.66] vs true [1.5, -2.0, 0.5]
training accuracy 0.85 in 10 iterations
L-BFGS reconstructs the action of the inverse Hessian from the last few (step,
gradient-change) pairs via the two-loop recursion -- O(m n) memory, no matrix stored --
giving Newton-like convergence on smooth problems that cripple first-order methods.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lbfgs.svgt-SNE: nonlinear dimensionality reduction that preserves neighborhoods
High-dimensional data usually lives on a low-dimensional manifold, and we want to SEE it: a 2-D map where points near in the original space stay near. Raw projection preserves large distances but smears local structure; t-SNE, van der Maaten and Hinton's method, preserves NEIGHBORHOODS instead, which is why its maps reveal clusters so vividly, and it is the default visualization for high-dimensional data across machine learning and computational biology. It is a probabilistic matching: in high-D, the similarity of point j to i is the probability i picks j as a neighbor under a Gaussian whose width is tuned per point so the effective neighbor count equals a target PERPLEXITY (a soft k adapting to local density); these are symmetrized into a joint distribution P. In the 2-D map, similarities use a heavy-tailed STUDENT-t kernel (1/(1+d^2)) -- the crucial trick that lets moderate-distance points spread out and cures the 'crowding problem'. t-SNE then moves the map points by gradient descent to minimize KL(P||Q). This module implements perplexity calibration by binary search on each point's bandwidth, the symmetric joint P, the Student-t affinities, and KL-gradient descent with momentum and early exaggeration, verified that P is a valid symmetric distribution, that the perplexity search hits its target, that the KL divergence falls over training, that well-separated clusters map to well-separated 2-D groups, and that a neighbourhood-preservation (trustworthiness) score is high.
t-SNE: nonlinear dimensionality reduction that preserves neighborhoods 80 points in 8 dimensions, 4 true clusters KL divergence: 1.639 (start) -> 0.095 (end) trustworthiness (k=8): 0.989 (1.0 = perfect neighbor preservation) 2-D separation: inter-cluster distance is 28.0x the intra-cluster distance t-SNE matches per-point Gaussian neighbor probabilities in high-D to a heavy-tailed Student-t kernel in 2-D, minimizing KL(P||Q) by gradient descent. The heavy tail cures the crowding problem, letting clusters breathe apart -- which is why the map is so clear. wrote C:\Users\acwic\symplectic-nbody\examples\output\tsne.svg
Wavelet trees: rank, select, and quantile over a sequence
A WAVELET TREE is a succinct data structure that turns a sequence over an alphabet into a balanced tree of bit vectors, answering a remarkable range of queries in O(log sigma) time with essentially the space of the sequence itself -- the machinery behind FM-indexes, compressed suffix arrays, and range-search structures in geometry and bioinformatics. It recursively partitions the ALPHABET: at each node the value range is split at its midpoint, and a bit vector marks whether each element falls in the upper half (1) or lower half (0); elements going left and right form the children's subsequences, which recurse on their half of the alphabet, so a value is encoded by its root-to-leaf bit path. Every query walks that O(log sigma)-deep tree using RANK on the bit vectors to map an index into the correct child. From this one structure fall RANK (occurrences of a value in a prefix), SELECT (position of the j-th occurrence, by walking back up), QUANTILE (the k-th smallest value in a range -- a range-median generalization array scans cannot do in sublinear time), and RANGE_COUNT (values in a positional range that fall in a value window). This module builds a wavelet tree over an integer sequence and implements all five, verified exhaustively against brute force: access reproduces the sequence, rank matches a prefix count, select matches an occurrence scan, quantile matches a sorted-slice lookup, and range-count matches a filtered scan, across hundreds of random sequences and queries.
Wavelet tree: rank, select, quantile, and range-count in O(log sigma)
sequence: [3, 1, 4, 1, 5, 2, 6, 5, 3, 5, 0, 7, 2, 6, 4]
alphabet range: [0, 7]
RANK -- how many times a value appears in a prefix:
rank(5, 15) = 3 (brute 3)
rank(5, 8) = 2 (brute 2)
rank(2, 15) = 2 (brute 2)
SELECT -- position of the j-th occurrence (0-indexed):
select(5, 0) = 4 (seq[4] = 5)
select(5, 2) = 9 (seq[9] = 5)
select(3, 1) = 8 (seq[8] = 3)
QUANTILE -- k-th smallest value in a range (range median and friends):
quantile([0,15), k=7) = 4 (sorted slice [7] = 4)
quantile([2,9), k=0) = 1 (sorted slice [0] = 1)
quantile([2,9), k=6) = 6 (sorted slice [6] = 6)
quantile([4,11), k=3) = 5 (sorted slice [3] = 5)
RANGE_COUNT -- values in a positional range that fall in a value window:
range_count([0,15), 3..5) = 7 (brute 7)
range_count([0,8), 0..2) = 3 (brute 3)
range_count([5,15), 4..7) = 6 (brute 6)
The tree splits the alphabet at its midpoint level by level; each node stores one bit
per element (upper half = 1, lower half = 0). Every query walks the O(log sigma)-deep
tree, using bit-rank to map an index into the correct child -- succinct and fast.
wrote C:\Users\acwic\symplectic-nbody\examples\output\wavelet_tree.svgFibonacci heaps: O(1) amortized decrease-key
A priority queue with fast DECREASE-KEY is the engine behind Dijkstra's shortest paths and Prim's minimum spanning tree, which do O(m) decrease-keys and O(n) extract-mins. A binary heap does every operation in O(log n); the FIBONACCI HEAP of Fredman and Tarjan improves the bound to O(m + n log n) by making INSERT, MERGE, FIND-MIN, and DECREASE-KEY run in O(1) AMORTIZED time, paying the logarithmic cost only at EXTRACT-MIN. The trick is LAZINESS: the heap is a forest of heap-ordered trees in a circular root list, so insert just drops in a node and merge just concatenates two lists. Cleanup happens only at extract-min, which promotes the minimum's children to roots and CONSOLIDATES trees of equal degree (like binary addition) until all root degrees are distinct. DECREASE-KEY lowers a key and, if heap order breaks, CUTS the node to the root list; a MARK bit triggers a CASCADING CUT the second time a node loses a child, which keeps trees bushy enough that degrees obey Fibonacci-number bounds -- hence the name. This module implements the full heap (insert, find-min, extract-min, decrease-key, delete, merge) with node handles, plus a Dijkstra built on it, verified against a binary heap over a 2000-operation random stream, that a drained heap yields sorted order, that merge preserves all elements, that the maximum root degree stays within the O(log n) Fibonacci bound, and that Dijkstra on the Fibonacci heap matches a binary-heap Dijkstra across 40 random graphs.
Fibonacci heap: O(1) amortized insert, merge, and decrease-key
inserted [27, 25, 60, 33, 36, 64, 1, 33, 87, 26, 71, 53]
extract-min order: [1, 25, 26, 27, 33, 33, 36, 53, 60, 64, 71, 87] (sorted: True)
decrease-key: min starts at 30; after decreasing d(90)->5, min is 5
merge two heaps -> size 6, min 1 (O(1) list concatenation)
Maximum root degree vs heap size (should stay below the Fibonacci bound log_phi(n)):
n= 10: max degree 3 (bound 4.6)
n= 50: max degree 5 (bound 8.1)
n= 100: max degree 6 (bound 9.5)
n= 500: max degree 8 (bound 12.9)
n= 1000: max degree 9 (bound 14.4)
n= 5000: max degree 12 (bound 17.7)
Dijkstra shortest paths from node 0 (classic 6-node graph): [0, 7, 9, 20, 20, 11]
(0->2->5->4 gives 9+2+9 = 20 to node 4)
A Fibonacci heap stays lazy: insert and merge just splice into a circular root list,
and decrease-key cuts a node to the roots (cascading up via mark bits). Only extract-min
consolidates equal-degree trees -- which is what keeps decrease-key O(1) amortized and
improves Dijkstra to O(m + n log n).
wrote C:\Users\acwic\symplectic-nbody\examples\output\fibonacci_heap.svgTreaps: balanced search trees by randomization
A binary search tree is fast only while balanced, and the classic balancers (AVL, red-black) achieve that with intricate rotation cases. A TREAP earns balance almost for free: give every key a random PRIORITY and keep the tree a binary-search-tree on the KEYS and a heap on the PRIORITIES (tree + heap = treap). Because priorities are random, the treap's shape equals that of a BST built by inserting the keys in random order -- balanced with high probability, O(log n) expected height, and none of the case analysis. The structure rests on two primitives: SPLIT cuts a treap into keys < k and keys >= k, and MERGE joins two treaps where all of one's keys precede the other's, taking the higher-priority root to preserve heap order. Insert is split-then-merge-in; delete is merge-around. That split/merge pair -- which AVL and red-black trees do not expose naturally -- makes treaps the tree of choice for slicing and splicing ordered sequences. Augmenting each node with its subtree SIZE turns it into an ORDER-STATISTICS tree (k-th smallest and rank in O(log n)). This module implements insert, delete, membership, select, rank, split, and merge with a seeded RNG, verified against a sorted list: in-order traversal is always sorted, a 3000-operation random insert/delete stream keeps the contents equal to a reference set, select and rank match a sorted array, split produces the correct partition, merge reassembles the original, and even under adversarial sorted insertion the height stays near 2 log2(n) where a plain BST would degrade to n-1.
Treap: a balanced BST from random priorities (tree + heap)
inserted [50, 30, 70, 20, 40, 60, 80, 10, 25, 65]
in-order (sorted): [10, 20, 25, 30, 40, 50, 60, 65, 70, 80]
height 6 for 10 keys
Order statistics:
0-th smallest = 10
4-th smallest = 40
9-th smallest = 80
rank of 40 = 4 (keys smaller than it)
rank of 65 = 7 (keys smaller than it)
split at 50 -> lower [10, 20, 25, 30, 40]
upper [50, 60, 65, 70, 80]
merge back -> [10, 20, 25, 30, 40, 50, 60, 65, 70, 80]
Expected height stays near 2*log2(n) (random priorities = random insertion order):
(an adversarial sorted insertion into a plain BST would give height n-1)
n= 100 (sorted insert): treap height 12 vs 2log2(n)=13.3 vs unbalanced BST would be 99
n= 1000 (sorted insert): treap height 26 vs 2log2(n)=19.9 vs unbalanced BST would be 999
n= 10000 (sorted insert): treap height 32 vs 2log2(n)=26.6 vs unbalanced BST would be 9999
n= 50000 (sorted insert): treap height 39 vs 2log2(n)=31.2 vs unbalanced BST would be 49999
Every key gets a random priority; the treap is a BST on keys and a heap on priorities,
so its shape equals a BST built from a random insertion order -- balanced with high
probability. Split and merge (which AVL/red-black trees don't expose) fall out for free.
wrote C:\Users\acwic\symplectic-nbody\examples\output\treap.svgSplay trees: self-adjusting search trees that keep hot keys near the root
Most balanced trees maintain an explicit invariant; a SPLAY TREE, from Sleator and Tarjan, keeps NO balance information at all, yet achieves O(log n) AMORTIZED time through one move called SPLAYING. Every access, insert, or delete rotates the touched node all the way to the root, in careful pairs -- ZIG-ZIG when node and parent lean the same way, ZIG-ZAG when opposite, a single ZIG at the top -- that lift the node while roughly halving the depth of everything on its path, so long paths pay for themselves by becoming short. This buys properties fixed-balance trees lack: the WORKING-SET property (recently used keys sit near the root, so temporally-local workloads run far faster than log n per access) and STATIC OPTIMALITY (on any access sequence a splay tree is within a constant factor of the best static tree built with full knowledge of the frequencies -- with no tuning). This module implements insert, delete, membership, find-min/max, predecessor/successor, and ordered traversal, splaying on every access, verified against a sorted set: in-order traversal stays sorted through a 3000-operation random stream, the BST and parent-pointer invariants hold throughout, predecessor/successor match a sorted array, the most-recently-accessed key is always at the root, and hammering a small hot set drives the average access depth well below log2(n) -- the working-set property in action.
Splay tree: self-adjusting BST, hot keys rise to the root
inserted 7 keys; in-order: [20, 30, 40, 50, 60, 70, 80]
access 20 -> root is now 20
access 80 -> root is now 80
access 40 -> root is now 40
Working-set property: with skewed access, splay beats fixed O(log n).
Build a tree of 4000 keys, then draw accesses from a Zipf-like skewed distribution;
measure the average depth of the accessed node BEFORE it is splayed.
hot fraction 1.00 (|hot|=4000): avg access depth 16.49 (balanced tree ~ 12.0)
hot fraction 0.50 (|hot|=2000): avg access depth 15.48 (balanced tree ~ 12.0)
hot fraction 0.20 (|hot|= 800): avg access depth 14.08 (balanced tree ~ 12.0)
hot fraction 0.10 (|hot|= 400): avg access depth 12.64 (balanced tree ~ 12.0)
hot fraction 0.05 (|hot|= 200): avg access depth 11.72 (balanced tree ~ 12.0)
hot fraction 0.02 (|hot|= 80): avg access depth 10.32 (balanced tree ~ 12.0)
hot fraction 0.01 (|hot|= 40): avg access depth 8.67 (balanced tree ~ 12.0)
As the access pattern concentrates, the splay tree's average access depth drops far
below log2(n): hot keys live near the root. A balanced tree pays log2(n) every time,
regardless of how skewed the workload is. Splaying is O(log n) amortized either way.
wrote C:\Users\acwic\symplectic-nbody\examples\output\splay_tree.svgVan Emde Boas trees: integer sets with O(log log u) successor
A balanced search tree does membership, insert, and SUCCESSOR/PREDECESSOR in O(log n). But when the keys are integers from a bounded universe {0, ..., u-1}, the VAN EMDE BOAS TREE does all of them in O(log log u) -- exponentially better in the universe size. For u = 2^32 that is about five steps per operation regardless of how many keys are stored, which makes it the theoretical champion for the predecessor problem and the basis of fast integer priority queues, IP routing tables, and integer sorting. The structure recursively divides the universe by its SQUARE ROOT: each key splits into a HIGH half (which of sqrt(u) clusters) and a LOW half (position within it); a node holds sqrt(u) child vEB trees plus a SUMMARY vEB tree over sqrt(u) recording which clusters are non-empty. The genius is storing each node's MIN and MAX directly and NOT recursing on the min -- that caps the work at one recursive call per level, so T(u) = T(sqrt u) + O(1) = O(log log u). Successor checks the current cluster, and if the answer is not there consults the summary to jump to the next non-empty cluster in a single step. This module implements insert, delete, membership, min, max, successor, and predecessor over a universe rounded to a power of two, verified against a reference sorted set: membership after a 4000-operation random stream, correct min and max, successor and predecessor matching a linear scan at every point in the universe, and the successor-walk from the minimum reproducing the sorted key list.
Van Emde Boas tree: integer sets with O(log log u) successor/predecessor
universe u=64, inserted keys, sorted: [5, 9, 17, 21, 33, 42, 58]
min 5, max 58
successor(21) = 33, successor(42) = 58
predecessor(33) = 21, predecessor(5) = None
member(17)? True member(18)? False
Recursion depth per operation grows like log2(log2(u)) -- barely at all:
(a balanced BST would pay log2(n); vEB pays log2 log2 u regardless of n)
u = 2^ 4: vEB recursion depth 2 (log2 log2 u = 2.00), BST would be up to 4 levels deep
u = 2^ 8: vEB recursion depth 3 (log2 log2 u = 3.00), BST would be up to 8 levels deep
u = 2^16: vEB recursion depth 4 (log2 log2 u = 4.00), BST would be up to 16 levels deep
u = 2^24: vEB recursion depth 4 (log2 log2 u = 4.58), BST would be up to 24 levels deep
u = 2^32: vEB recursion depth 5 (log2 log2 u = 5.00), BST would be up to 32 levels deep
u = 2^48: vEB recursion depth 5 (log2 log2 u = 5.58), BST would be up to 48 levels deep
u = 2^64: vEB recursion depth 6 (log2 log2 u = 6.00), BST would be up to 64 levels deep
In a universe of 2^24 = 16777216 values with 5 keys:
successor(123456) = 5000000
predecessor(9999999) = 5000000
sorted keys: [10, 123456, 5000000, 9999999, 16000000]
Each key splits into a high half (which cluster) and low half (position in it); the
tree recurses on the SQUARE ROOT of the universe, and storing min/max directly caps the
recursion at one call per level -- so T(u) = T(sqrt u) + O(1) = O(log log u).
wrote C:\Users\acwic\symplectic-nbody\examples\output\van_emde_boas.svgSparse tables and binary lifting: O(1) range minimum, O(log n) LCA
Some queries deserve constant time. The RANGE MINIMUM QUERY -- the minimum of any subrange of a fixed array -- is answered by a SPARSE TABLE in O(1) after O(n log n) preprocessing, exploiting that min is IDEMPOTENT: overlapping ranges combine without double-counting, so any interval is covered by just TWO precomputed power-of-two blocks. The table stores the minimum of every block [i, i+2^k); a query takes the length, finds the largest fitting power 2^k, and returns min(table[l][k], table[r-2^k][k]) -- two lookups, always. The same doubling idea on trees gives BINARY LIFTING for the LOWEST COMMON ANCESTOR: store each node's 2^k-th ancestor, then to find LCA(u,v) lift the deeper node to the other's depth and lift both by the largest jumps that keep them apart until their parents coincide -- O(log n) per query, which also yields the tree distance for free. These are the standard tools for static range and tree-ancestor queries in competitive programming, compilers, and phylogenetics. This module implements a generic sparse table for any idempotent operation (min, max, gcd), a specialized RMQ, and a binary-lifting LCA with depth and distance, verified against brute force: the sparse table matches a direct scan over every subrange, the LCA matches a naive ancestor-walk for every pair of nodes on many random trees, and the tree distance matches a BFS shortest path.
Sparse table: O(1) range minimum, and binary-lifting LCA
array: [5, 2, 8, 1, 9, 3, 7, 4, 6, 0, 11, 2]
Range minimum queries (each answered by exactly two table lookups):
min[1,5) = 1 (brute 1)
min[3,9) = 1 (brute 1)
min[0,12) = 0 (brute 0)
min[6,8) = 4 (brute 4)
Same table idea for max: max[3,9) = 9 (brute 9)
Lowest common ancestor (binary lifting), on a 9-node tree:
LCA(7, 4) = 1, distance = 3
LCA(7, 8) = 0, distance = 6
LCA(3, 4) = 1, distance = 2
LCA(8, 6) = 2, distance = 3
RMQ works because min is idempotent: any range is covered by two overlapping
power-of-two blocks, so a query is min(table[l][k], table[r-2^k][k]) -- always O(1).
LCA lifts the deeper node to the other's depth, then both jump up by shrinking
powers of two until their parents meet -- O(log n) using the 2^k-ancestor table.
wrote C:\Users\acwic\symplectic-nbody\examples\output\sparse_table.svgPollard's rho: factoring integers in sqrt(p) steps
Multiplying two large primes is easy; recovering them from the product is the FACTORIZATION problem that RSA's security rests on. Trial division works only up to tiny factors -- factoring a 40-digit number that way would outlast the universe. POLLARD'S RHO finds a non-trivial factor of a composite n in expected O(n^{1/4}) time and O(1) space; for a factor p that is about sqrt(p) iterations, versus the p that trial division needs. The idea is a probabilistic collision: iterate a pseudo-random x -> x^2 + c (mod n), and by the BIRTHDAY PARADOX the sequence cycles after about sqrt(p) steps MODULO a prime factor p, so when two iterates collide mod p but not mod n their difference shares p with n and gcd(|x_i - x_j|, n) reveals it. Brent's variant (used here) batches the gcd computations to run faster; POLLARD'S p-1 is a complementary trick that shatters RSA primes whose p-1 is smooth. This module implements deterministic Miller-Rabin primality, Brent's Pollard rho, Pollard p-1, and a full recursive prime factorization, plus Euler's totient and divisor count from the factors, verified against brute-force trial division and a sieve: the product of the returned factors equals the input and every factor is prime, it recovers both primes of random semiprimes (the RSA case) including a 17-digit one, primality matches a sieve, and totient and divisor counts match brute-force enumeration.
Pollard's rho: factoring integers in ~sqrt(smallest factor) steps
600851475143 = 71 * 839 * 1471 * 6857
1000000016000000063 = 1000000007 * 1000000009
720720 = 2^4 * 3^2 * 5 * 7 * 11 * 13
254803968 = 2^20 * 3^5
1000036000099 = 1000003 * 1000033
RSA-style semiprimes (product of two primes) -- the case RSA relies on being hard:
10403 = 101 * 103
100160063 = 10007 * 10009
1000036000099 = 1000003 * 1000033
10000004400000259 = 100000007 * 100000037
Pollard's rho finds a factor p in about sqrt(p) iterations (birthday paradox),
where trial division would need p/2. For a 15-digit semiprime that is the
difference between a few thousand steps and a hundred million.
Number-theoretic functions from the factorization:
n=36: phi(n)=12, number of divisors=9
n=100: phi(n)=40, number of divisors=9
n=720720: phi(n)=138240, number of divisors=240
wrote C:\Users\acwic\symplectic-nbody\examples\output\pollard_rho.svgPerlin noise: smooth gradient fields for procedural generation
Random numbers are jagged; nature is smooth. PERLIN NOISE, Ken Perlin's 1983 invention (and an Academy Award winner for its use in film), produces a random-looking but CONTINUOUS field -- values that vary smoothly across space with no visible grid -- the foundation of procedurally generated terrain, clouds, textures, and fire. Unlike white noise, it has controllable feature size and looks organic because it is differentiable. The construction is GRADIENT noise: lay an integer lattice over space and give each lattice point a pseudo-random unit gradient (deterministically, from a hashed permutation table, so the field is reproducible and infinite); to evaluate a point, take at each surrounding corner the dot product of that corner's gradient with the vector to the point, and interpolate with the SMOOTHSTEP fade 6t^5 - 15t^4 + 10t^3 (whose first and second derivatives vanish at the ends, which is what makes it seamless). The noise passes through zero on the grid and undulates between. Layering copies at doubling frequency and halving amplitude -- FRACTAL BROWNIAN MOTION -- adds detail at every scale, the standard terrain recipe. This module implements reproducible 1-D and 2-D Perlin noise and fBm, verified that the noise is exactly zero at integer lattice points (the defining property), stays within bounds, is deterministic per seed and continuous (a 1e-3 step moves the output by under 0.003), has near-zero mean over a large region, and that fBm with more octaves adds high-frequency detail while staying bounded.
Perlin noise: smooth pseudo-random gradient fields
Gradient noise is exactly zero at integer lattice points:
noise2(3,5) = 0.00e+00, noise2(0,0) = 0.00e+00
but noise2(3.5, 5.5) = 0.3535 (smooth between)
Over a 100x100 sample grid:
range [-0.861, 0.861], mean -0.0085
Fractal Brownian motion (summing octaves at doubling frequency, halving amplitude):
1 octave(s): sample spread 0.962 (more octaves -> more fine detail)
2 octave(s): sample spread 0.746 (more octaves -> more fine detail)
4 octave(s): sample spread 0.550 (more octaves -> more fine detail)
6 octave(s): sample spread 0.527 (more octaves -> more fine detail)
Each lattice point holds a random gradient vector; the noise at a point is the
smoothstep-interpolated dot product of the surrounding gradients with the offset
vectors. Layering octaves (fBm) is the standard recipe for terrain, clouds, and fire.
wrote C:\Users\acwic\symplectic-nbody\examples\output\perlin.svgWave function collapse: procedural generation by constraint propagation
WAVE FUNCTION COLLAPSE fills a grid with tiles so that every adjacency obeys local rules, producing outputs that look hand-authored -- coherent maps, textures, levels -- rather than random. The name is a quantum metaphor: each cell starts in a SUPERPOSITION of every tile, and the algorithm repeatedly OBSERVES (collapses) the most-constrained cell to one tile, then PROPAGATES that choice, deleting any option a neighbour can no longer support -- the discrete constraint-satisfaction loop of arc consistency. Observation picks the lowest-ENTROPY cell (fewest remaining options) and collapses it by weight; propagation runs a worklist, shrinking neighbour option sets until the grid is arc-consistent, and if a cell's options ever hit zero the attempt is a CONTRADICTION and restarts. The adjacency rules -- which tiles may sit beside which, per direction -- are the whole specification, turning a tiny rule set into endless consistent worlds. This module implements tiled WFC with per-direction rules, weighted collapse, lowest-entropy observation, full propagation, contradiction detection with restart, and a seeded RNG, verified that every generated grid strictly satisfies the rules (no forbidden neighbour anywhere, across 20 seeds), that a seed reproduces its grid, that an over-constrained rule set is reported unsatisfiable rather than producing garbage, and that a rule set forcing a unique tiling produces exactly it (a perfect checkerboard).
Wave function collapse: coherent maps from local adjacency rules
Coastline map (rule: land 'L' and sea 'S' may never touch; coast 'C' between them):
.~~.~~~~~~~.~.~~..~~~~~~~~~~
~.~.~.~~~~~~~~...~.~~~~~~~~~
.#.~~~~.~~.~.~~...~~~~~~~~~.
~.~~~~~.~~~~~~.~~~..~~~.~~.~
~~~~.~~~~~..~~~~.~..~~..~~~~
.~~~~~~.~~.~~.~.~~~~~~~~~.~.
~~~~~~.~~~~~~~~.~~~~~~~~.#.#
~.~~~~~~~~..~~~~~~~~~~.~~.~.
~~~~~~~.~.~~~~~~~~~~.~~.~~~~
~~~~~~~.~~~.~.~~..~~~.~~~~~.
.~~.~~~~~~.~~~~~..~..~.....~
.~~~~~~~~~~~~~~~.~.~~~~~~~~~
~~~~~...~~.~~~~~.~.~~~~~.~~.
~...~~~~.~~~~.~~~~~~~.~~~~~~
.~~~~~~.~.~..~.~~~~~~.....~~
#..~~~~~~~~.~..~~~.~.~~~~~~~
satisfies all adjacency rules: True
land-sea direct adjacencies (should be 0): 0
Forced checkerboard rules -> a perfect 2-coloring: True
Each cell begins as a superposition of all tiles. WFC repeatedly collapses the
lowest-entropy (most-constrained) cell to one tile, then propagates: neighbours lose
any option the new choice forbids, cascading until the grid is arc-consistent. A
contradiction (a cell with no options left) triggers a restart.
wrote C:\Users\acwic\symplectic-nbody\examples\output\wave_function_collapse.svgThe Hungarian algorithm: optimal assignment in O(n^3)
Given n workers, n jobs, and a COST for each pairing, which one-to-one assignment minimizes the total? This ASSIGNMENT PROBLEM appears everywhere -- taxis to riders, tasks to machines, tracked objects to detections between video frames. Brute force tries all n! permutations; the HUNGARIAN ALGORITHM (Kuhn-Munkres, 1955) finds the exact optimum in O(n^3), one of the first polynomial algorithms for a combinatorial problem. It rests on an invariant: subtracting a constant from a full row or column does not change WHICH assignment is optimal, only shifts every total equally. So the algorithm reduces rows and columns to expose zeros, selects n INDEPENDENT zeros (one per row and column -- a valid assignment), and when fewer than n exist, covers all zeros with a minimum set of lines (Konig's theorem) and subtracts the smallest uncovered value to create new zeros without breaking the old, until n independent zeros -- the optimum -- appear. This module implements the O(n^3) potential/augmenting-path form for rectangular matrices (padded to square), for both minimization and maximization, verified against brute-force search over all permutations (matching the exact optimum over 60 random square matrices and up to n=8), the row/column reduction invariant, identity and hand-worked matrices, and rectangular padding.
The Hungarian algorithm: optimal assignment in O(n^3)
Cost matrix (hours for each worker to do each job):
cook clean drive shop
Ann 9 11 14 11
Bob 6 15 13 10
Cy 12 13 6 8
Dot 11 9 10 12
Optimal assignment (total 32.0 hours):
Ann -> shop (11 h)
Bob -> cook (6 h)
Cy -> drive (6 h)
Dot -> clean (9 h)
Brute force over all 4! = 24 permutations: 32 (matches: True)
Greedy (pick each row's cheapest free job): 34 -> 2.0 hours worse than optimal
The algorithm reduces rows and columns to expose zeros, selects n independent zeros
(one per row and column), and when fewer than n exist, covers the zeros with a minimum
set of lines and shifts the smallest uncovered value to create new ones -- until an
optimal set of independent zeros appears. O(n^3), versus n! for brute force.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hungarian.svgDynamic time warping: aligning time series that vary in speed
Two recordings of the same spoken word, two gait cycles, two heartbeats trace the same shape at different, non-uniformly varying speeds -- so a point-by-point Euclidean comparison badly mismatches them. DYNAMIC TIME WARPING finds the optimal NON-LINEAR alignment: it stretches and compresses the time axis of one series to match the other, returning both the minimal alignment cost and the WARPING PATH. DTW was the backbone of speech recognition before deep learning and remains central to gesture and signature recognition and time-series clustering. It is dynamic programming over an n x m cost grid: cell (i, j) is the cheapest cost to align the first i and first j points, equal to the local distance between points i and j plus the minimum of three neighbours (the match, insert, and delete moves). The corner cell is the DTW distance, and backtracking the minimizing choices recovers the monotone warping path pairing each point of one series with one or more of the other. A SAKOE-CHIBA BAND confines the path near the diagonal, speeding it to O(n*w) and forbidding pathological warps. This module implements DTW distance, warping-path recovery, an optional band, and a multi-dimensional variant, verified against an independent DP and known properties: identical series have zero distance, DTW is symmetric and non-negative, it is invariant to time stretching (duplicating points changes nothing), it crushes the Euclidean distance on shifted signals, and the recovered path is monotone with unit steps and its summed cost equals the distance.
Dynamic time warping: aligning series that vary in speed
two 30-point signals of the same shape at different speeds:
DTW distance: 2.731
Euclidean distance: 17.900 (naive point-by-point)
-> DTW is 6.6x smaller: it warps the time axis to match the shapes
warping path length: 37 matched index pairs
Stretch invariance: DTW(base, 3x-stretched-base) = 0.0 (exactly zero)
Sakoe-Chiba band (limits the warp window):
unconstrained DTW: 2.731
band=5 DTW: 3.517 (band restricts the path, so distance >= unconstrained)
DTW fills a cost grid where cell (i,j) is the local distance plus the cheapest of its
three predecessors (match / insert / delete). The bottom-right cell is the DTW distance,
and backtracking the minimizing moves recovers the monotone warping path.
wrote C:\Users\acwic\symplectic-nbody\examples\output\dtw.svgThe P-square algorithm: streaming quantiles in constant memory
Computing a QUANTILE the exact way -- the median, the 99th percentile -- requires storing and sorting all the data, impossible for a stream of billions or a sensor reporting forever. The P-SQUARE ALGORITHM (Jain and Chlamtac, 1985) estimates any quantile using just FIVE markers and O(1) memory, never storing the samples, with accuracy that improves as more data arrives -- the classic tool for latency percentiles (p50/p95/p99) in monitoring without keeping every request time. It tracks five markers along the data: the running minimum and maximum, the current quantile estimate, and two markers halfway between; each has a height and a position, plus a DESIRED position that grows linearly with the sample count so the markers stay spread at the target quantile. As each value arrives it is slotted into a marker cell, positions increment, and any marker that drifts more than one step from its desired position is nudged back -- its height adjusted by PARABOLIC interpolation through its neighbours, falling back to linear if the parabola would break the ordering. The middle marker is the estimate. This module implements the single-quantile estimator and a multi-quantile histogram, verified against exact quantiles from the full sorted data: on uniform, normal, and exponential streams the estimate is within a small error of the true quantile (a p99 within 0.12% using 20 floats instead of storing 200000 samples), the median of a symmetric stream is near its centre, the min and max markers are exact, and a constant stream returns the constant.
P-square: streaming quantiles (latency percentiles) in constant memory
streamed 200000 latency samples (heavy-tailed, in ms):
percentile P-square exact error
p50 49.96 49.97 0.02%
p90 106.51 106.50 0.01%
p95 134.39 134.51 0.09%
p99 212.30 212.05 0.12%
Memory: P-square keeps 5 markers per quantile = 20 floats total,
versus 200000 samples (1562 KB) that exact computation would need to store and sort.
The p95 estimate converges to the true value as more data arrives:
after 100 samples: p95 estimate 144.61 (true so far 144.10)
after 1000 samples: p95 estimate 142.98 (true so far 138.02)
after 10000 samples: p95 estimate 133.94 (true so far 134.04)
after 50000 samples: p95 estimate 135.24 (true so far 135.27)
after 200000 samples: p95 estimate 134.39 (true so far 134.51)
Five markers track the min, max, the target quantile, and two midpoints; each new
sample nudges the markers toward their desired positions using parabolic interpolation.
No samples are stored -- ideal for p99 latency monitoring on a firehose of requests.
wrote C:\Users\acwic\symplectic-nbody\examples\output\p2_quantile.svgTarjan's strongly connected components and the condensation DAG
In a directed graph, a STRONGLY CONNECTED COMPONENT is a maximal set of vertices where every vertex can reach every other -- the graph's cycles collapsed into equivalence classes. Shrinking each SCC to a single node yields the CONDENSATION, which is always a DAG, so every directed graph is a directed-acyclic graph of its cycles -- the structure behind dependency and dead-code analysis, deadlock detection, 2-SAT, and the web graph's block model. TARJAN'S ALGORITHM finds all SCCs in a single depth-first search in O(V + E). It gives each vertex a DISCOVERY INDEX and a LOW-LINK (the smallest index reachable from its subtree via at most one back-edge to a vertex still on the DFS stack); a vertex whose low-link equals its own index is the ROOT of an SCC, and everything pushed onto an auxiliary stack after it forms that component. Because the low-link propagates cycle reachability, the roots partition the vertices into exactly the SCCs, discovered in reverse topological order of the condensation. This module implements Tarjan's SCC iteratively (no recursion-depth limit), the condensation DAG, a Kahn topological sort, and a cycle test, verified against a brute-force mutual-reachability check (two vertices share an SCC iff each reaches the other) over 100 random graphs, that the condensation is always acyclic, that SCCs come in reverse topological order, that a cycle is one component and a DAG is all singletons, and on a 5000-cycle that the iterative DFS survives without hitting Python's recursion limit.
Tarjan's SCC: collapsing a directed graph's cycles into a DAG
8 vertices, 11 edges
strongly connected components (reverse topological order):
component 0: [5, 6, 7]
component 1: [3, 4]
component 2: [0, 1, 2]
condensation DAG super-edges: [(1, 0), (2, 1)]
condensation topological order: [2, 1, 0]
condensation is acyclic: True
original graph has a cycle: True
condensation has a cycle: False
A single depth-first search assigns each vertex a discovery index and a low-link (the
smallest index reachable via one back-edge from its subtree). A vertex whose low-link
equals its index is an SCC root; the stack above it forms the component. O(V+E).
wrote C:\Users\acwic\symplectic-nbody\examples\output\tarjan_scc.svg2-SAT: satisfying two-literal clauses in linear time
Boolean SATISFIABILITY is NP-complete in general, but the restricted case where every clause has at most TWO literals is solvable in LINEAR time -- a rare island of tractability inside an intractable problem, and a beautiful application of strongly connected components. The trick is the IMPLICATION GRAPH: a clause (a OR b) is equivalent to two implications, (not a implies b) and (not b implies a), so build a directed graph with two vertices per variable (the literal and its negation) and an edge for each implication. A truth assignment is consistent exactly when no variable x has x and NOT x forced together -- which happens iff x and NOT x lie in the SAME strongly connected component (each would then imply the other). So the formula is SATISFIABLE iff no variable shares an SCC with its negation, checkable by Tarjan's SCC in O(V + E); and when satisfiable, an assignment reads straight off the SCC order because the condensation is a DAG. This module (built on this project's Tarjan SCC) adds clauses and implications, tests satisfiability, and extracts a satisfying assignment, verified against brute force over all 2^n assignments: the verdict always matches whether any assignment satisfies the formula, every returned assignment actually satisfies every clause (over 300 random formulas), and the canonical contradiction (x) AND (not x) and the four-clause unsatisfiable formula are classified correctly.
2-SAT: satisfying two-literal clauses in linear time via SCCs Formula: (F1 v F2) AND (~F1 v F3) AND (~F2 v ~F3) SATISFIABLE. One assignment: F1=T, F2=F, F3=T verifies against every clause: True brute force agrees it is satisfiable: True Unsatisfiable formula: (a v b)(a v ~b)(~a v b)(~a v ~b) solver verdict: UNSATISFIABLE reason: some variable and its negation land in the same SCC of the implication graph Each clause (a v b) becomes two implications (~a -> b) and (~b -> a). The formula is satisfiable iff no variable shares a strongly connected component with its own negation; when it does, x -> ~x -> x forces a contradiction. Assignment reads off the SCC order. wrote C:\Users\acwic\symplectic-nbody\examples\output\two_sat.svg
Dormand-Prince RK45: adaptive-step ODE integration
Solving an ODE y' = f(t, y) numerically hinges on STEP SIZE: too large loses accuracy or stability, too small wastes work where the solution is smooth. ADAPTIVE methods choose the step at every point to keep the local error near a target tolerance -- big steps through calm regions, tiny steps through rapid change. The DORMAND-PRINCE method (RK45, the engine of MATLAB's ode45 and SciPy's solve_ivp) pairs a 5th- and a 4th-order Runge-Kutta formula that SHARE their stage evaluations, so their difference is a cheap estimate of the local truncation error -- used to accept or reject each step and rescale it by (tol/error)^(1/5). Seven stages per step (six new plus one reused via the FSAL property) make it efficient. This module implements adaptive Dormand-Prince RK45 for scalar and vector ODEs, forward or backward in time, with optional sampling at requested times, verified against closed-form solutions: exponential decay and growth to the requested tolerance, a harmonic oscillator conserving energy to 2e-9 over ten periods and returning exactly to its start, a logistic curve, and a 2-frequency oscillator matching cos(2t) -- with tightening the tolerance shrinking the error (1.6e-5 at tol=1e-4 down to 2e-11 at tol=1e-10) and faster dynamics correctly demanding more steps.
Dormand-Prince RK45: adaptive-step ODE solving
y' = -y, y(0)=1: y(5) = 0.00673795 (exact e^-5 = 0.00673795)
error 1.98e-10 in 54 adaptive steps
Harmonic oscillator over 10 periods:
energy drift 2.54e-09 (should be ~0), 1469 steps
Tolerance controls accuracy AND effort:
tol=1e-04: 8 steps, error 1.60e-05
tol=1e-06: 16 steps, error 1.63e-07
tol=1e-08: 35 steps, error 1.79e-09
tol=1e-10: 83 steps, error 1.97e-11
Van der Pol oscillator (mu=5.0, nonlinear relaxation oscillation):
450 adaptive steps; step size ranges 0.0124 to 0.1089
-> the solver takes 9x bigger steps in the smooth stretches
than through the sharp switch-backs -- exactly where a fixed step would waste work.
Each step computes a 5th- and a 4th-order estimate from shared stage evaluations; their
difference estimates the local error, which accepts/rejects the step and rescales it by
(tol/error)^(1/5). Big steps in calm regions, tiny steps through rapid change.
wrote C:\Users\acwic\symplectic-nbody\examples\output\rk45.svgCORDIC: trigonometry and logarithms with only shifts and adds
How does a calculator or an FPGA with no hardware multiplier compute sin, cos, or a logarithm? CORDIC (COordinate Rotation DIgital Computer, Volder 1959) evaluates a whole family of transcendental functions using nothing but ADDITION, SUBTRACTION, and BIT SHIFTS -- no multiplication, no division, no lookup of the function itself. It powered the first scientific calculators and still lives in FPGAs and DSPs wherever a multiplier is scarce. The trick is iterative ROTATION by ever-smaller angles whose tangents are exact powers of two, so 'multiplying' by the rotation is just a bit shift. To get cos and sin, start at (1, 0) and rotate toward the target angle in steps of arctan(2^-i), adding or subtracting the shifted coordinates depending on whether the running angle is short of or past the target; after n steps the point is (K cos, K sin) for a fixed gain K divided out at the end. This is CIRCULAR mode; flipping a sign gives HYPERBOLIC mode (exp, ln, sqrt), and a VECTORING variant computes atan2 and hypot. This module implements all of them on shift-and-add updates with precomputed angle and gain tables, verified against the math library across their ranges: cos/sin to ~1e-9 over [-2pi, 2pi], atan2 in all four quadrants, hypot, and exp/ln/sqrt to high precision (with range reduction), the Pythagorean identity everywhere, and the circular gain matching its analytic product.
CORDIC: sin, cos, exp, ln, sqrt using only shifts and additions
Circular functions (vs math library):
cos(0.5000) = 0.8775825619 (math 0.8775825619)
sin(0.5000) = 0.4794255386 (math 0.4794255386)
cos(1.0000) = 0.5403023059 (math 0.5403023059)
sin(1.0000) = 0.8414709848 (math 0.8414709848)
cos(1.0472) = 0.5000000000 (math 0.5000000000)
sin(1.0472) = 0.8660254038 (math 0.8660254038)
Vectoring mode (rectangular -> polar):
atan2(1, 1) = 0.7853981634 (pi/4 = 0.7853981634)
hypot(3, 4) = 5.0000000000 (= 5)
Hyperbolic mode (exp, ln, sqrt):
exp(2) = 7.3890560989 (math 7.3890560989)
ln(10) = 2.3025850930 (math 2.3025850930)
sqrt(50) = 7.0710678119 (math 7.0710678119)
Rotation converging on angle 1.1 rad (each step is a shift-add):
after 1 steps: angle 0.785398 (error 3.15e-01)
after 3 steps: angle 1.004067 (error 9.59e-02)
after 5 steps: angle 1.066003 (error 3.40e-02)
after 7 steps: angle 1.112867 (error 1.29e-02)
after 9 steps: angle 1.101148 (error 1.15e-03)
after 11 steps: angle 1.100172 (error 1.72e-04)
Each iteration rotates by +/- arctan(2^-i); because tan of the step is a power of two,
the rotation is a bit shift, not a multiply. The running angle homes in on the target,
and the final x,y are cos and sin (after dividing out the fixed CORDIC gain).
wrote C:\Users\acwic\symplectic-nbody\examples\output\cordic.svgSavitzky-Golay: smoothing that preserves peaks, and noisy differentiation
Smoothing noisy data usually blunts its features -- a moving average flattens peaks and shifts edges. The SAVITZKY-GOLAY filter, the standard in analytical chemistry, smooths by fitting a low-degree POLYNOMIAL to a sliding window by least squares and taking the fitted value at the centre. Because a polynomial follows a peak's curvature, it removes noise while preserving peak height and width far better than a moving average -- the default for spectroscopy and chromatography, where SHAPE matters. The elegant fact is that for evenly-spaced points the least-squares fit reduces to fixed CONVOLUTION COEFFICIENTS depending only on window size and degree, so the whole filter is one convolution. Taking the DERIVATIVE of the fitted polynomial at the centre yields a smoothed estimate of the signal's derivative -- differentiating noisy data, normally a disaster, becomes stable, invaluable for finding inflection points and rates. This module computes coefficients for any odd window, degree, and derivative order and applies the filter with edge handling, verified against exact references: a polynomial of degree <= the filter degree passes through unchanged (the defining property), the derivative mode recovers the analytic derivative, smoothing a noisy sine cuts the MSE to the clean signal, the coefficients have unit DC gain (sum 1) for smoothing and zero for derivatives, and it beats a moving average at preserving a Gaussian peak's height (0.996 vs 0.842).
Savitzky-Golay: smoothing that keeps peak shapes, and stable differentiation
degree-2, window-5 smoothing coefficients: [-0.0857, 0.3429, 0.4857, 0.3429, -0.0857]
(sum = 1.0000, the classic -3 12 17 12 -3 / 35)
MSE to the clean signal:
noisy input: 0.0048
moving average: 0.0028
Savitzky-Golay: 0.0008
peak-2 height (true 1.450): SG 1.434, moving avg 1.264
-> the moving average flattens the sharp peak; SG keeps it
Smoothed 1st derivative finds peak locations (zero crossings): [61, 100, 131, 148, 163, 196]
(true peaks near 60 and 130 -- differentiating raw noisy data would be hopeless)
Savitzky-Golay fits a low-degree polynomial to a sliding window by least squares and
takes the centre value -- a fixed convolution. The polynomial follows a peak's
curvature, so unlike a moving average it smooths without blunting features, and its
derivative gives a stable estimate of the signal's slope.
wrote C:\Users\acwic\symplectic-nbody\examples\output\savitzky_golay.svgLZW: adaptive dictionary compression
LEMPEL-ZIV-WELCH is the dictionary compressor behind GIF, early TIFF, and Unix `compress`. Its charm is that it needs NO explicit dictionary in the output and NO two passes: encoder and decoder build the SAME dictionary independently as they go, so the compressed stream is just a sequence of integer codes. It replaces repeated SUBSTRINGS with single codes, and the longer and more repetitive the input, the longer the matched substrings become -- so its ratio improves as it learns the data. The encoder starts with every single byte (codes 0..255), reads the longest string w already in the dictionary, and on the next character c either extends (if w+c is known) or outputs the code for w, adds w+c as a new entry, and restarts at c. The decoder mirrors this one step behind, with the classic KwKwK edge case (a code referring to the entry about to be built) resolved by the rule that the entry is the previous string plus its own first character. This module implements byte-oriented LZW compression and decompression with an optional capped code width (GIF-style dictionary reset), verified by exhaustive round-tripping: decompress(compress(x)) == x for 300 random byte strings, repetitive data, text, all-same and all-distinct inputs, and the empty string; that repetitive input yields far fewer codes than its length; that the KwKwK case decodes correctly; and that the capped-width reset round-trips with codes staying in range.
LZW: adaptive dictionary compression (GIF / Unix compress)
input : TOBEORNOTTOBEORTOBEORNOT (24 bytes)
codes : [84, 79, 66, 69, 79, 82, 78, 79, 84, 256, 258, 260, 265, 259, 261, 263] (16 codes)
decompresses exactly: True
Codes > 255 are dictionary entries the encoder built from repeated substrings;
the decoder rebuilds the identical dictionary with no side channel.
Compression ratio (codes / bytes) vs repetition:
1 repetitions ( 20 bytes): ratio 1.000
2 repetitions ( 40 bytes): ratio 0.750
5 repetitions ( 100 bytes): ratio 0.540
10 repetitions ( 200 bytes): ratio 0.400
25 repetitions ( 500 bytes): ratio 0.264
50 repetitions ( 1000 bytes): ratio 0.190
100 repetitions ( 2000 bytes): ratio 0.137
Repetitive 2700 bytes: ratio 0.047 (great)
Random 2700 bytes: ratio 0.983 (near 1 -- incompressible)
Dictionary entries learned from 'the quick brown fox' x50: 189
LZW replaces repeated substrings with single codes, and because both sides build the
same dictionary as they read, the compressed stream is just integer codes -- no
dictionary transmitted, one pass, and the ratio improves the longer the patterns run.
wrote C:\Users\acwic\symplectic-nbody\examples\output\lzw.svgConvolutional codes and the Viterbi decoder: error correction for noisy channels
A CONVOLUTIONAL CODE protects a bit stream by feeding it through a shift register and emitting, each step, several output bits that are XORs of the current and recent inputs. Unlike a block code it has MEMORY -- each output depends on a sliding window -- spreading each bit's information across many transmitted bits, so a burst of channel errors can be undone. Convolutional codes carried the Voyager images home and run in every satellite and GSM modem. Decoding a noisy stream means finding the input whose encoded output is CLOSEST (minimum Hamming distance) to what arrived; brute force over 2^n inputs is hopeless, but the VITERBI ALGORITHM does it in linear time by dynamic programming on the TRELLIS -- at each step keeping, for every register state, the single survivor path with least accumulated error, then tracing back the survivors to recover the maximum-likelihood sequence. This module implements a rate-1/n encoder for arbitrary generator polynomials and a Viterbi decoder with zero-tail termination, verified that a clean channel decodes exactly, that it corrects every single-bit error and most well-separated double errors, that Viterbi achieves the same minimum distance as a brute-force search over 60 noisy trials, and that the classic (7,5) code shows a clear coding gain -- 95% decode success at a 5% channel error rate where uncoded transmission manages only 41%.
Convolutional coding + Viterbi decoding over a noisy channel
rate-1/2 code, constraint length 3, 4 trellis states
message (10 bits): 1011001011
encoded (24 bits): 111000010111111000010111
received (2 bit errors at [3, 11]):
111100010110111000010111
decoded (10 bits): 1011001011
recovered the original exactly: True
Decode success vs channel bit-error rate (200 messages of 16 bits each):
error rate coded uncoded
0.00 1.00 1.00
0.02 0.99 0.73
0.05 0.95 0.41
0.10 0.74 0.16
0.15 0.51 0.07
0.20 0.22 0.04
0.30 0.02 0.01
The encoder runs the message through a shift register, emitting XOR combinations that
spread each bit across several outputs. Viterbi finds the maximum-likelihood path
through the trellis -- the transmitted sequence closest to what arrived -- in linear time.
wrote C:\Users\acwic\symplectic-nbody\examples\output\convolutional_code.svgEar-clipping: triangulating any simple polygon
Triangulating a polygon -- splitting it into non-overlapping triangles that exactly tile its interior -- is the first step of almost all polygon processing: rendering (GPUs draw only triangles), finite-element meshing, area and centroid computation, and collision geometry. The TWO EARS THEOREM guarantees every simple polygon with more than three vertices has at least two EARS -- a vertex whose neighbours can be joined by a diagonal lying entirely inside, cutting off a triangle that contains no other vertex. EAR CLIPPING finds an ear, snips it as a triangle, and repeats on the smaller polygon until a triangle remains -- the simplest robust triangulation, O(n^2). A vertex is an ear when it is CONVEX (the polygon turns the right way, so the diagonal is interior) and no other vertex lies INSIDE the candidate triangle; the algorithm fixes the winding from the signed area so 'convex' is consistent, then clips and rescans. An n-vertex polygon yields exactly n-2 triangles. This module triangulates convex or concave polygons in either winding order, verified against exact references: the triangle count is always n-2, the triangle areas sum exactly to the polygon's area (shoelace) with no gaps or overlaps, every triangle's centroid lies inside the polygon, and stars, L-shapes, arrows, and a deeply non-convex comb all triangulate correctly.
Ear-clipping triangulation: any simple polygon into n-2 triangles
star: 10 vertices -> 8 triangles (n-2 = 8)
polygon area 4.7023, triangle sum 4.7023, match True
L-shape: 6 vertices -> 4 triangles (n-2 = 4)
polygon area 5.0000, triangle sum 5.0000, match True
arrow: 7 vertices -> 5 triangles (n-2 = 5)
polygon area 8.0000, triangle sum 8.0000, match True
The two-ears theorem guarantees every simple polygon with >3 vertices has an EAR --
a convex vertex whose diagonal stays inside and whose triangle holds no other vertex.
Clip the ear, repeat on the smaller polygon; n-2 triangles later, done. O(n^2).
wrote C:\Users\acwic\symplectic-nbody\examples\output\ear_clipping.svgMulti-layer perceptron and backpropagation
A single linear classifier cannot learn XOR -- no straight line separates its classes. The MULTI-LAYER PERCEPTRON stacks layers of neurons with NONLINEAR activations, and with even one hidden layer becomes a UNIVERSAL APPROXIMATOR that can represent any continuous function -- the foundation of deep learning. It learns by BACKPROPAGATION, the chain rule applied systematically: a forward pass gives the prediction and loss, then the loss's gradient flows BACKWARD layer by layer, each layer computing its weights' contribution to the error from the gradient handed down from above. Because each layer reuses the downstream gradient, the whole gradient is computed in one backward pass costing the same as the forward pass -- the efficiency that makes training deep networks feasible. Gradient descent then nudges every weight down its gradient. This module implements a feedforward MLP with configurable layers and activations (sigmoid, tanh, ReLU), full backprop, and mini-batch gradient descent with momentum, verified that its analytic gradients match finite-difference gradients to 8e-11 (the definitive backprop-correctness test), that it learns the XOR a linear model cannot (loss to 4e-5, all four points correct), that it fits a nonlinear regression, that it separates blobs to 100% and a circular decision boundary to 99%, and that training is reproducible from a seed.
Multi-layer perceptron + backpropagation
XOR (not linearly separable):
[0, 0] -> 0.004 (target 0)
[0, 1] -> 0.991 (target 1)
[1, 0] -> 0.991 (target 1)
[1, 1] -> 0.011 (target 0)
loss 0.1269 -> 0.000038 over 3000 epochs
a linear model gets only 3/4 right (XOR needs a hidden layer)
Circular decision boundary (inside a disk): accuracy 0.993
a linear boundary could never separate a disk from its surroundings
Backpropagation is the chain rule run backward: a forward pass computes the loss, then
the gradient flows back layer by layer, each reusing the gradient handed down from
above, so the whole gradient costs one backward pass. Gradient descent does the rest.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mlp.svgMarkov decision processes: value iteration and policy iteration
A MARKOV DECISION PROCESS frames sequential decision-making under uncertainty -- a robot navigating, inventory being restocked -- with STATES, ACTIONS, stochastic TRANSITIONS, REWARDS, and a DISCOUNT that trades immediate against future reward. The goal is a POLICY that maximizes expected discounted return, and MDPs are the foundation of reinforcement learning. VALUE ITERATION repeatedly applies the BELLMAN OPTIMALITY backup V(s) <- max_a sum_s' P(s'|s,a)[R + gamma V(s')]; because this is a contraction (it shrinks the gap to the true values by gamma each sweep) it converges geometrically to the optimal value function, whose greedy policy is optimal. POLICY ITERATION alternates exact policy evaluation with greedy improvement, converging in a handful of rounds. This module implements both for a finite MDP plus a stochastic gridworld builder, verified that value and policy iteration reach the SAME optimal policy and values, that the result satisfies the Bellman optimality equation (zero residual), that a corridor yields a straight-to-goal policy, that a higher discount values distant rewards more, that a hand-built MDP gives the exact expected value, that stochastic slip is handled, and that value iteration's error contracts by exactly the discount factor each sweep on a self-looping MDP.
Markov decision process: value iteration and policy iteration
5x4 gridworld, goal at (4, 3), obstacles [(1, 1), (1, 2), (3, 1)], 10% slip:
value iteration converged in 26 sweeps
policy iteration converged in 3 rounds
same policy: True, same values: True
Bellman residual: 1.22e-11 (zero = optimal fixed point)
Optimal policy (G = goal, # = obstacle):
> > > > G
^ # > > ^
^ # ^ # ^
> > ^ > ^
State values (higher = closer to the goal along the optimal path):
+0.56 +0.69 +0.82 +0.98 +0.00
+0.46 ## +0.70 +0.83 +0.98
+0.36 ## +0.58 ## +0.83
+0.29 +0.37 +0.47 +0.57 +0.69
Value iteration applies the Bellman optimality backup until the value function stops
changing (a contraction, converging geometrically). Policy iteration alternates exact
policy evaluation with greedy improvement, converging in a handful of rounds. Both find
the same optimal policy -- the action in each state that maximizes expected return.
wrote C:\Users\acwic\symplectic-nbody\examples\output\mdp.svgMulti-armed bandits: the exploration-exploitation tradeoff
A MULTI-ARMED BANDIT is the purest model of decision under uncertainty: a row of slot machines paying from unknown distributions, and a gambler choosing which to pull each round to maximize reward. The tension is EXPLORATION versus EXPLOITATION -- pull the arm that looks best, or try an under-sampled one that might be better? Bandits model A/B testing, ad selection, and clinical trials. Performance is REGRET: reward lost by not always pulling the truly best arm; a good policy drives average regret toward zero. This module implements three classics: EPSILON-GREEDY (exploit the best-so-far, explore randomly with probability epsilon), UCB1 (optimism under uncertainty -- pull the arm maximizing its mean plus a confidence bonus sqrt(2 ln t / n) that shrinks with sampling, giving provably logarithmic regret with no tuning), and THOMPSON SAMPLING (keep a Beta posterior per arm, sample one value from each, pull the argmax -- Bayesian probability-matching). Verified against exact references: every learning policy vastly outperforms random selection, UCB1 and Thompson achieve sublinear regret (average regret falls toward zero as rounds grow), all policies identify the best arm as the most-pulled, UCB1's regret grows logarithmically (far slower than linearly), decaying epsilon beats fixed, and a dominant arm is found fast.
Multi-armed bandit: exploration vs exploitation
5 arms with payout probabilities [0.2, 0.5, 0.75, 0.4, 0.6]
best arm is #2 (p = 0.75), over 3000 rounds:
policy total reward regret best-arm %
random 1449 758.5 20.9%
epsilon-greedy 2151 92.4 90.4%
UCB1 2092 150.9 78.1%
Thompson 2210 35.6 94.3%
Regret is reward lost versus always pulling the best arm. Random selection accrues
regret linearly; UCB1 and Thompson learn which arm is best and their regret flattens
(grows only logarithmically), driving the average regret per round toward zero.
UCB1 after 500 rounds (mean + confidence bonus per arm):
arm 0: pulled 26, mean 0.269, UCB 0.961 (true 0.2)
arm 1: pulled 59, mean 0.508, UCB 0.967 (true 0.5)
arm 2: pulled 269, mean 0.762, UCB 0.977 (true 0.75)
arm 3: pulled 38, mean 0.395, UCB 0.967 (true 0.4)
arm 4: pulled 108, mean 0.630, UCB 0.969 (true 0.6)
wrote C:\Users\acwic\symplectic-nbody\examples\output\bandit.svgQ-learning: model-free reinforcement learning from experience
Value iteration solves an MDP when you KNOW its transitions and rewards. But an agent dropped into an unknown world can only ACT and observe. MODEL-FREE reinforcement learning learns to act optimally from raw experience alone. Q-LEARNING learns the ACTION-VALUE Q(s, a) -- the expected return of taking action a in state s then acting optimally -- purely from sampled transitions (s, a, r, s'), and its greedy policy converges to the optimum. The update is TEMPORAL-DIFFERENCE learning: nudge Q(s,a) toward r + gamma max_a' Q(s',a'), the reward plus the discounted value of the best next action; the bracketed TD ERROR is the surprise between prediction and outcome. Q-learning is OFF-POLICY -- it learns the optimal Q while exploring epsilon-greedily. SARSA is its on-policy cousin, bootstrapping from the action actually taken. This module implements tabular Q-learning and SARSA over a step-based gridworld (the agent never sees the transition model), verified against value iteration as ground truth: the learned greedy policy matches the optimal policy, reaches the goal in the optimal number of steps (on 4x3 and 6x6 gridworlds), the learned Q-values approach the MDP optimum, a learned agent reaches the goal far faster than a random walker (5 vs 57 steps), returns improve over training, a higher learning rate speeds early learning, and SARSA also solves the task.
Q-learning: model-free reinforcement learning from experience
6x5 gridworld, 4 obstacles, goal (5, 4)
Q-learning saw only (state, action, reward, next-state) samples -- never the model.
after 3000 episodes: policy matches value iteration on 21/25 states
greedy rollout: Q-learning 9 steps vs value-iteration optimum 9
SARSA (on-policy) rollout: 9 steps
Learned policy (G = goal, # = obstacle):
> > > > > G
^ ^ ^ # > ^
^ ^ ^ < # ^
^ # # > > ^
^ > > > > ^
The TD update nudges Q(s,a) toward r + gamma max_a' Q(s',a') after each step -- learning
the optimal action-values from raw experience, off-policy, while exploring epsilon-greedily.
wrote C:\Users\acwic\symplectic-nbody\examples\output\q_learning.svgReverse-mode automatic differentiation
How does a deep-learning framework compute the gradient of a loss with respect to millions of parameters? Not by hand-derived formulas and not by finite differences (slow and inexact), but by REVERSE-MODE AUTOMATIC DIFFERENTIATION -- the engine behind PyTorch's autograd and JAX's grad -- which computes exact derivatives of any function built from elementary operations at the cost of about one extra evaluation, regardless of the number of inputs. It records operations as they run: each numeric VALUE remembers the operation that produced it and its parents, so an expression becomes a computation graph; the forward pass computes the result, and the BACKWARD pass walks the graph in reverse topological order, applying each node's LOCAL DERIVATIVE to accumulate the gradient flowing back from its children -- backpropagation generalized from neural-net layers to any composition. Because it sweeps from the single scalar output back to all inputs, one pass yields the entire gradient vector, which is why it dominates machine learning. This module implements a Value type with +, -, *, /, **, and exp/log/sin/cos/tanh/relu/sqrt, each recording its local derivative, plus a topological backward pass, verified that gradients match finite differences across a nonlinear function suite and hand-derived symbolic derivatives, that they accumulate correctly when a value is reused (graph diamonds), that a gradient-descent loop using autodiff reaches the analytic optimum, and that a 3-parameter a*sin(bx)+c model is trained to recover its true parameters exactly.
Reverse-mode automatic differentiation: exact gradients through any expression
f(x,y,z) = sin(xy) + e^(z^2)*x - y/z at [0.8, 1.2, 0.5]
value: -0.553588
autodiff gradient: [+1.97225, -1.54118, +5.82722]
finite-diff gradient: [+1.97225, -1.54118, +5.82722]
max difference: 1.75e-10 (autodiff is exact)
One backward pass computes the WHOLE gradient vector, regardless of the number of
inputs -- the property that makes it the engine of deep learning.
Training a 3-parameter model y = a*sin(b*x) + c by autodiff gradient descent:
true params: a=2.0, b=1.5, c=0.5
learned params: a=2.000, b=1.500, c=0.500
loss 3.0001 -> 0.000000
wrote C:\Users\acwic\symplectic-nbody\examples\output\autodiff.svgShamir's secret sharing: any k of n pieces reconstruct the secret
How do you store a secret -- a master key, a launch code -- so no single person holds it, yet any sufficiently large group can recover it? SHAMIR'S SECRET SHARING solves this with geometry: a polynomial of degree k-1 is uniquely determined by any k of its points, but k-1 points reveal NOTHING. Hide the secret as the constant term of a random degree-(k-1) polynomial over a finite field GF(p), hand each participant one point (x, f(x)) as their SHARE, and any k shares interpolate the polynomial and read off the secret f(0) -- while any k-1 shares leave every possible secret equally likely, information-theoretic security, not merely computational. The arithmetic lives in integers mod a prime so division is exact; reconstruction is LAGRANGE INTERPOLATION at x=0 with modular inverses. The scheme is (k, n)-THRESHOLD: any k of the n shares suffice, any fewer are useless. This module implements splitting a secret integer (or byte string) into shares over a 256-bit prime field and reconstructing it, verified that any k of the n shares reconstruct the secret exactly, that every k-subset gives the same answer, that no k-1 subset recovers it, that the shares span the field, that a byte-string secret round-trips, and on the boundary cases k=1 and k=n.
Shamir's secret sharing: any k of n shares reconstruct the secret
secret: 1234567890
(3, 5) threshold -> 5 shares, any 3 reconstruct:
share 1: 4151667051347653500082243584139699646936...
share 2: 6054314218818774026761417985512402722793...
share 3: 5707941502413361580037523204118109227568...
share 4: 3112548902131416159910559239956819161262...
share 5: 9847345341704557308737624593897323309203...
reconstruct from shares 1,2,3: 1234567890
reconstruct from shares 3,4,5: 1234567890
reconstruct from just 2 shares: 224901988387653297340306918276... (wrong -- reveals nothing)
byte secret b'attack at dawn' split (2,4):
reconstruct from 2 shares: b'attack at dawn'
The geometry: the secret is the y-intercept f(0) of a degree-(k-1) polynomial. Each
share is a point on it. k points fix the polynomial uniquely (Lagrange interpolation
recovers f(0)); k-1 points leave infinitely many polynomials, so the secret is hidden.
wrote C:\Users\acwic\symplectic-nbody\examples\output\shamir.svgMerkle trees: compact proofs of membership in a dataset
A MERKLE TREE hashes a list of data blocks into a single ROOT such that any change to any block changes the root, and any single block can be proven to belong with a proof of only O(log n) hashes -- without revealing the other blocks. It is the structure behind blockchain transaction commitments, Git's content addressing, certificate transparency logs, and peer-to-peer file verification, turning 'is this item in the set?' from an O(n) download into an O(log n) proof. The leaves are the hashes of the blocks; each internal node hashes its two children; the root fingerprints the entire ordered dataset (flip one bit anywhere and it changes). To prove block i is included, supply the AUTHENTICATION PATH -- the sibling hash at each level from leaf to root -- and a verifier who trusts only the root recomputes the path and checks it lands on the root. This module builds a tree over byte blocks (SHA-256 with domain-separated leaf and node prefixes, the standard second-preimage defence), generates inclusion proofs, and verifies them, checked that a valid proof for every block verifies, that tampering with the block, the proof, or the root all fail, that the proof length is logarithmic (a million blocks need a 20-hash proof), that changing or reordering any block changes the root, and on odd block counts where the last node is promoted.
Merkle tree: O(log n) proofs that a block belongs to a dataset
8 transaction blocks -> Merkle root 51483b2847f13ec776c5681c...
Inclusion proof for block 3 ('tx: Dot->Eve 3'):
3 sibling hashes (log2(8) = 3):
left sibling: 72836eadad4c42f34826586e...
left sibling: 3674ff0918216c59cf6644d7...
right sibling: b308c557eb8da518dba34f35...
verifies against the root: True
(a verifier who trusts only the root confirms membership without the other blocks)
Tamper detection:
altered block verifies: False (rejected)
forged proof verifies: False (rejected)
Proof size grows logarithmically (download shrinks from O(n) to O(log n)):
8 blocks: proof ~ 3 hashes (96 bytes) vs 8 blocks to download
64 blocks: proof ~ 6 hashes (192 bytes) vs 64 blocks to download
1024 blocks: proof ~ 10 hashes (320 bytes) vs 1024 blocks to download
1000000 blocks: proof ~ 20 hashes (640 bytes) vs 1000000 blocks to download
Each leaf is a block's hash; each parent hashes its two children; the root fingerprints
the whole ordered set. An inclusion proof is the sibling hash at each level up to the
root -- recompute the path, check it lands on the trusted root. Flip any bit and it won't.
wrote C:\Users\acwic\symplectic-nbody\examples\output\merkle.svgThe traveling salesman problem: exact Held-Karp and 2-opt
The TRAVELING SALESMAN PROBLEM asks for the shortest tour visiting every city once and returning to the start -- the archetypal NP-hard optimization problem (the tour count grows as (n-1)!/2), yet the model for vehicle routing, circuit drilling, and logistics everywhere. Two complementary approaches: an EXACT dynamic program for small instances and fast HEURISTICS for large ones. HELD-KARP builds, for every SUBSET of cities and every possible last city, the shortest path from the start through that subset ending there -- reusing subproblems for O(n^2 2^n) instead of O(n!), making ~20 cities exactly solvable. The 2-OPT heuristic starts from any tour and repeatedly removes two edges and reconnects them the other way (reversing the segment between) whenever that shortens the tour, a local search that removes the self-crossings a good tour never has and lands within a few percent of optimal. This module implements Held-Karp, nearest-neighbour construction, and 2-opt over an arbitrary distance matrix, verified against brute-force permutation search that Held-Karp returns the true optimum on small instances, that 2-opt never worsens a tour and averages within ~1% of the Held-Karp optimum, that every tour is a valid permutation whose reported length matches its edges, that the Euclidean helper satisfies the triangle inequality, and on hand-checked squares and collinear points.
Traveling salesman: exact Held-Karp DP and the 2-opt heuristic
11 cities (Held-Karp exact, examining subsets not the 3.6M tours):
nearest-neighbour tour: 370.9
Held-Karp optimum: 349.0
2-opt from NN: 350.1
60 cities ((n-1)!/2 = astronomically many tours; 2-opt local search):
nearest-neighbour: 778.1
after 2-opt: 683.6 (12.1% shorter)
2-opt removes the self-crossings a good tour never has
Held-Karp builds shortest paths over every (subset, last-city) pair, reusing
subproblems for O(n^2 2^n) instead of O(n!). 2-opt repeatedly reverses a segment
between two edges whenever that shortens the tour -- fast, and within a few percent.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tsp.svgThe Poisson equation by relaxation: fields, potentials, and steady heat
The POISSON equation laplacian(u) = f governs the electrostatic potential of a charge, the steady-state temperature of a heated plate, and the pressure in incompressible flow; with zero source it is LAPLACE'S equation, whose solutions are HARMONIC -- every point the average of its surroundings, no interior hot spots. On a grid with fixed boundary values, the solution is found by RELAXATION: sweep the grid replacing each cell with the source-adjusted average of its four neighbours until the field stops changing. Three schemes trade simplicity for speed: JACOBI updates from the old values (simple, slow), GAUSS-SEIDEL uses freshly-updated values (about twice as fast), and SUCCESSIVE OVER-RELAXATION overshoots each correction by a factor omega in (1,2) -- with the optimal omega it converges an order of magnitude faster. This module solves the 2-D Poisson/Laplace equation with Dirichlet boundaries by all three methods, verified against exact references: Laplace with linear boundary data reproduces the exact linear harmonic solution, the discrete solution satisfies the mean-value property and the maximum principle (no interior extrema), a separable analytic harmonic solution sinh(kx)sin(ky) is matched to grid accuracy, a point charge gives a symmetric monotone potential, all three methods converge to the same field, and SOR beats Jacobi by 30x (158 vs 5261 sweeps on a 40x40 grid).
Poisson/Laplace by relaxation: steady-state fields on a grid
Steady-state heat on a 40x40 plate (hot left 100, cold right 0):
converged in 158 SOR sweeps; harmonic (mean-value error 4.02e-09)
center temperature: 48.7 (halfway between the edges)
Convergence speed (sweeps to tol=1e-8, 40x40 grid):
jacobi : 5261 sweeps
gauss_seidel : 2733 sweeps
sor : 158 sweeps
SOR over-relaxes each correction by omega in (1,2), converging an order faster.
Poisson for a point charge (grounded box): peak potential 37.10
residual |laplacian(u) - f|: 9.21e-09
Laplace's equation makes every interior point the average of its neighbours -- a
harmonic field, smooth with no interior hot spots. Relaxation just sweeps the grid
averaging until it settles; SOR accelerates by overshooting each averaging step.
wrote C:\Users\acwic\symplectic-nbody\examples\output\poisson.svgManacher's algorithm: every palindrome in linear time
Finding the LONGEST PALINDROMIC SUBSTRING naively takes O(n^2) or worse. MANACHER'S ALGORITHM does it in O(n) by computing, for every position, the RADIUS of the longest palindrome centered there, reusing the symmetry of already-found palindromes so no character is examined more than a constant number of times -- the definitive linear-time palindrome algorithm, used in bioinformatics and text processing. It handles even- and odd-length palindromes uniformly by inserting separators so every palindrome becomes odd-length with one center; a running scan keeps the RIGHTMOST palindrome found (center C, right edge R), and a new center i inside R inherits its MIRROR position's radius for free, expanding beyond it only when possible -- and each expansion advances R, so total work is linear. This module returns the per-center radii, the longest palindromic substring, the count of all palindromic substrings, and the odd/even radius profiles, verified against brute force: the longest palindrome matches an O(n^2) center-expansion reference and the total count matches a full substring enumeration over 500 random strings, the returned substring is always a genuine palindrome, and edge cases (empty, single char, all-same, all-distinct, a 2000-char worst case) all work.
Manacher's algorithm: all palindromes in O(n)
'babad' : longest 'bab' (3 chars), 7 palindromic substrings
'racecar' : longest 'racecar' (7 chars), 10 palindromic substrings
'abacaba' : longest 'abacaba' (7 chars), 12 palindromic substrings
'mississippi' : longest 'ississi' (7 chars), 20 palindromic substrings
'aabaaacaaab' : longest 'baaacaaab' (9 chars), 24 palindromic substrings
Verify 'aabaaacaaab': Manacher 24 == brute 24
Odd-length palindrome radius at each center of 'abacabadabacaba':
a b a c a b a d a b a c a b a
1 2 1 4 1 2 1 8 1 2 1 4 1 2 1
peak radius 8 at center 'd' (index 7) -> the whole string is a palindrome
The string is padded with separators so every palindrome is odd-length with one
center. A running scan keeps the rightmost palindrome found; a new center inherits its
mirror's radius for free and only expands beyond it -- so total work is linear.
wrote C:\Users\acwic\symplectic-nbody\examples\output\manacher.svgStoer-Wagner: the global minimum cut of a weighted graph
A minimum CUT splits a graph's vertices into two groups so the total weight of edges crossing between them is smallest. Unlike the s-t min cut (which fixes which side two vertices land on, solved by max-flow), the GLOBAL minimum cut asks for the cheapest cut over ALL ways of splitting -- the graph's weakest link, measuring network reliability and driving clustering and segmentation. The STOER-WAGNER algorithm finds it in O(V^3) with no flow computation, by a strikingly simple idea: MINIMUM CUT PHASES. Each phase grows a set from an arbitrary vertex, repeatedly adding the vertex most tightly connected to the current set (maximum-adjacency order); the last two vertices added, s and t, give a CUT-OF-THE-PHASE that is provably the minimum s-t cut for that pair, and the phase then MERGES s and t into one vertex (summing parallel weights) and repeats. After V-1 phases every pair has been implicitly considered, and the smallest cut-of-the-phase is the global minimum -- no augmenting paths, just orderings and merges. This module implements it, returning the cut weight and partition, verified against brute force over all vertex bipartitions of small graphs and on known graphs (the textbook example gives 4, a bridge gives its single edge, K_n gives n-1, a cycle gives two edges), that the partition achieves the reported weight, that parallel edges are summed, and that a disconnected graph gives a zero cut.
Stoer-Wagner: the global minimum cut of a weighted graph
Classic 8-vertex graph, 12 weighted edges:
global minimum cut weight: 4
partition: [2, 3, 6, 7] | [0, 1, 4, 5]
brute force confirms: 4
Two dense clusters joined by 2 weak links:
min cut 2 -> partition [4, 5, 6, 7] | [0, 1, 2, 3]
the cut finds exactly the weak bridges between the clusters
Each phase grows a set by repeatedly adding the most tightly-connected vertex; the last
vertex added gives a provable s-t min cut, then s and t merge. V-1 phases later, the
smallest cut seen is the global minimum -- no max-flow, no augmenting paths.
wrote C:\Users\acwic\symplectic-nbody\examples\output\stoer_wagner.svgChebyshev approximation: near-optimal fits that dodge the Runge phenomenon
Approximating a function by a polynomial sounds simple -- sample and interpolate -- but sampling at EQUALLY SPACED points is a trap: for many smooth functions the interpolation error EXPLODES near the interval ends as the degree rises (the RUNGE PHENOMENON). The fix is to sample at CHEBYSHEV POINTS, the projections of equally spaced points on a circle onto the interval, clustered toward the ends. Chebyshev interpolation converges for every continuous function and is NEAR-OPTIMAL -- its maximum error is within a small factor of the best possible polynomial of that degree, converging geometrically for analytic functions. A function is expanded as a sum of Chebyshev polynomials T_n = cos(n arccos x) with coefficients from samples at the Chebyshev nodes, evaluated stably by CLENSHAW'S RECURRENCE. Because T_n has equal-ripple extrema, the truncated series spreads its error evenly (equioscillation, the hallmark of minimax approximation) -- the foundation of function-approximation libraries and spectral methods. This module builds Chebyshev interpolants on any interval, verified that smooth functions (exp, sin, rationals) are matched to near machine precision, that error shrinks geometrically with degree, that on Runge's function Chebyshev stays bounded (error 0.007 at degree 24) where equispaced interpolation blows up (error 257), that a low-degree polynomial is recovered exactly, and that the nodes cluster at the ends.
Chebyshev approximation: near-optimal polynomial fits
Approximating exp(x) on [-1,1] -- error falls geometrically with degree:
degree 2: max error 5.65e-02
degree 4: max error 6.40e-04
degree 6: max error 3.62e-06
degree 8: max error 1.22e-08
degree 12: max error 4.26e-14
degree 16: max error 1.78e-15
Runge's function 1/(1+25x^2) -- Chebyshev vs equally-spaced nodes:
degree Chebyshev equispaced
8 0.1708 1.05
12 0.0692 3.66
16 0.0326 14.39
20 0.0153 59.77
24 0.0069 257.21
equispaced interpolation EXPLODES near the ends; Chebyshev nodes (clustered there)
tame it -- error spreads evenly across the interval (equioscillation).
Chebyshev nodes are the projections of equally spaced points on a circle onto the
interval, clustered at the ends. Expanding in Chebyshev polynomials T_n and evaluating
by Clenshaw recurrence gives a fit within a small factor of the best possible polynomial.
wrote C:\Users\acwic\symplectic-nbody\examples\output\chebyshev.svgGibbs sampling: drawing a joint distribution via its conditionals
Sampling from a high-dimensional joint distribution is hard, but sampling from its one-dimensional CONDITIONALS -- the distribution of one variable given the others fixed -- is often easy. GIBBS SAMPLING cycles through the variables, resampling each from its conditional given the current values of the rest; the sequence is a Markov chain whose stationary distribution is the target joint, so after a burn-in the samples are draws from it. It is the workhorse of Bayesian statistics (hierarchical models, LDA, image restoration) because the conditionals stay simple even when the joint is intractable. It is a special case of Metropolis-Hastings where every proposal is ACCEPTED (the ratio is exactly 1, since we propose from the true conditional), so no step is wasted. For a multivariate Gaussian each conditional is a 1-D Gaussian with a mean linear in the other coordinates and a variance from the precision matrix. This module implements Gibbs sampling for bivariate and general multivariate Gaussians (closed-form conditionals) and a generic user-conditional sampler, verified against exact references: the sampled mean and covariance of a correlated bivariate Gaussian converge to the true parameters, the correlation matches the target (0.735 recovered exactly), a 3-D Gaussian's full covariance is recovered, negative and zero correlations are handled, and a generic discrete conditional sampler reproduces a known joint distribution.
Gibbs sampling: draw a joint distribution via its conditionals
Correlated bivariate Gaussian (target mean [2.0, -1.0], cov [[3.0, 1.8], [1.8, 2.0]]):
sampled mean: [1.979, -1.013]
sampled covariance: [[3.01, 1.80], [1.80, 2.00]]
correlation: 0.735 (target 0.735)
Each step resamples x | y then y | x from their 1-D Gaussian conditionals -- every
proposal accepted (acceptance ratio exactly 1), so no step is wasted.
4-D Gaussian: sampled mean [0.01, 1.00, 2.00, 3.00] (target [0, 1, 2, 3])
diagonal variances: [1.97, 1.48, 1.00, 1.20] (target [2.0, 1.5, 1.0, 1.2])
Gibbs sampling is the engine of Bayesian computation: even when the joint is
intractable, the one-dimensional conditionals are usually simple, and cycling through
them builds a Markov chain that converges to the joint after burn-in.
wrote C:\Users\acwic\symplectic-nbody\examples\output\gibbs.svgLouvain community detection: the natural clusters in a network
Real networks break into COMMUNITIES -- groups of nodes more densely connected inside than between -- and finding them reveals structure without being told how many groups to look for. The quality measure is MODULARITY: how much denser the within-community edges are than a random graph with the same degrees. Maximizing it is NP-hard, but the LOUVAIN METHOD is a fast greedy heuristic that is the de-facto standard for community detection at scale. It alternates two phases until modularity stops improving: LOCAL MOVING, where each node joins whichever neighbour's community gives the largest modularity gain (computable in O(degree)), and AGGREGATION, where each community collapses into a super-node and the process recurses on the smaller graph. Each phase only raises modularity and the graph shrinks each round, so it converges quickly to a hierarchical partition. This module implements Louvain on a weighted undirected graph, returning the community assignment and achieved modularity, verified against exact references: the reported modularity matches a direct computation, on graphs with planted communities (dense cliques joined by sparse bridges) it recovers exactly those communities (four groups, modularity 0.66), a single clique stays one community, a complete graph gives low modularity, the found partition beats both trivial partitions, weighted edges are respected, and a ring of triangles is split correctly.
Louvain community detection: finding a network's natural clusters
20 nodes, 44 edges (4 planted groups + 4 bridges)
Louvain found 4 communities, modularity 0.6591
planted group 0 ([0, 1, 2, 3, 4]): community label(s) {0} <- recovered
planted group 1 ([5, 6, 7, 8, 9]): community label(s) {1} <- recovered
planted group 2 ([10, 11, 12, 13, 14]): community label(s) {2} <- recovered
planted group 3 ([15, 16, 17, 18, 19]): community label(s) {3} <- recovered
Modularity of the trivial partitions (for comparison):
all in one community: 0.0000
every node its own: -0.0506
Louvain: 0.6591 <- highest
Louvain greedily moves each node to the neighbouring community that most increases
modularity, then collapses communities into super-nodes and recurses. Each phase only
raises modularity, so it converges fast to a strong hierarchical partition.
wrote C:\Users\acwic\symplectic-nbody\examples\output\louvain.svgMatrix-chain multiplication: the optimal order by dynamic programming
Matrix multiplication is associative -- (AB)C = A(BC) -- but the WORK is not: multiplying a p x q matrix by q x r costs p*q*r operations, so the order of products can change the total cost by orders of magnitude. Given the dimensions, which PARENTHESIZATION minimizes the scalar multiplications? The number of orderings is a Catalan number (exponential), but DYNAMIC PROGRAMMING solves it in O(n^3) -- the textbook example of optimal substructure. The insight: the best way to multiply matrices i..j splits at some k into (i..k)(k+1..j) with both halves themselves optimal, so m[i][j] = min over k of m[i][k] + m[k+1][j] + p_{i-1} p_k p_j, filled by increasing chain length, with the base case m[i][i] = 0. Recording the minimizing split reconstructs the parenthesization. This is the canonical interval DP, shared with optimal binary search trees and polygon triangulation. This module computes the minimum cost and optimal parenthesization, verified against brute-force search over all Catalan-many orderings for short chains that the DP finds the true minimum, that the reconstructed order achieves that cost, that it beats the naive left-to-right order on skewed dimensions (94% saved on one example), and on the classic CLRS instance (15125).
Matrix-chain multiplication: the cheapest order to multiply a chain
6 matrices with dimensions:
A1: 30 x 35
A2: 35 x 15
A3: 15 x 5
A4: 5 x 10
A5: 10 x 20
A6: 20 x 25
optimal parenthesization: ((A1(A2A3))((A4A5)A6))
optimal cost: 15,125 scalar multiplications
left-to-right cost: 40,500
savings: 62.7%
A skewed chain where order matters even more:
dims [50, 5, 100, 5, 100, 5]: optimal 6,375 vs left-to-right 100,000 (94% saved)
optimal order: (A1((A2A3)(A4A5)))
The number of parenthesizations grows as a Catalan number (exponential), but the
DP fills an n x n cost table in O(n^3): m[i][j] = min over split k of the cost of the
two halves plus multiplying them. Recording each split reconstructs the optimal order.
wrote C:\Users\acwic\symplectic-nbody\examples\output\matrix_chain.svgLongest increasing subsequence: patience sorting in O(n log n)
The LONGEST INCREASING SUBSEQUENCE is the longest set of elements, in their original order, that strictly increases -- not necessarily contiguous. It measures how sorted a sequence is and appears in card games, computational biology (the longest consistently ordered gene run between genomes), and the analysis of permutations. The naive DP is O(n^2); PATIENCE SORTING does it in O(n log n) by dealing the sequence like solitaire: each number is placed on the leftmost PILE whose top is greater-or-equal, or starts a new pile. The number of piles at the end EQUALS the LIS length, and because the pile tops stay sorted, the right pile is found by BINARY SEARCH -- the log factor. Back-pointers to the top of the pile to the left at placement time reconstruct the actual subsequence. This module computes the length and a witnessing subsequence, with strict, non-decreasing, and longest-decreasing variants, verified against a brute-force O(2^n) search that the length is optimal and the returned subsequence is genuinely increasing and a real subsequence, against the O(n^2) DP over hundreds of random sequences, and on known cases (sorted gives n, reversed gives 1, all-equal gives 1 strict but n non-decreasing).
Longest increasing subsequence: patience sorting in O(n log n)
sequence: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
longest increasing subsequence (length 6): [1, 2, 3, 5, 7, 9]
patience-sorting piles (leftmost pile whose top >= card):
pile 0: [3, 1, 1]
pile 1: [4, 2]
pile 2: [5, 5, 3]
pile 3: [9, 6, 5]
pile 4: [8, 7]
pile 5: [9, 9]
number of piles = 6 = LIS length 6
strict vs non-decreasing on [1,3,3,5,2,4,4,6]:
strict: length 4
non-decreasing: length 6 (equal elements allowed)
longest DECREASING subsequence: length 4, [9, 6, 5, 3]
Each card is placed on the leftmost pile whose top is >= it (binary search), or starts
a new pile. The pile count equals the LIS length; back-pointers to the previous pile's
top at placement time reconstruct the actual subsequence. O(n log n).
wrote C:\Users\acwic\symplectic-nbody\examples\output\lis.svgContinued fractions: the best rational approximations of a real
Every real number expands as a CONTINUED FRACTION x = a0 + 1/(a1 + 1/(a2 + ...)), found greedily: take the integer part, invert the remainder, repeat. Truncating gives the CONVERGENTS p_k/q_k, which are the BEST RATIONAL APPROXIMATIONS -- no fraction with a smaller denominator is closer. This is why 22/7 and 355/113 are the famous approximations of pi, how gear ratios and calendar leap-years are chosen, and how the eventually-periodic expansion of a quadratic irrational solves Pell's equation. The convergents follow the recurrence p_k = a_k p_{k-1} + p_{k-2}, q_k = a_k q_{k-1} + q_{k-2}, always in lowest terms, straddling x and closing in with error bounded by 1/(q_k q_{k+1}) -- so a large partial quotient (pi's 292) makes the previous convergent (355/113, good to 7 digits) exceptional. This module computes the expansion of a real or exact fraction, its convergents, and the best rational within a denominator bound, verified that pi's convergents are 3, 22/7, 333/106, 355/113, that a finite expansion recovers its rational exactly, that each convergent is the best approximation for its denominator (checked against all smaller ones), that the golden ratio is all ones and sqrt(2) is [1;2,2,...], that convergents alternate around and converge to the target, and that best-approximation matches brute force within a denominator bound.
Continued fractions: the best rational approximations of a real
pi = [3; 7, 15, 1, 292, 1, 1, 1, ...]
e = [2; 1, 2, 1, 1, 4, 1, 1, ...]
golden ratio = [1; 1, 1, 1, 1, 1, 1, 1, ...]
sqrt(2) = [1; 2, 2, 2, 2, 2, 2, 2, ...]
Convergents of pi (each the best rational for its denominator):
3 = 3.0000000000 error 1.42e-01
22/7 = 3.1428571429 error 1.26e-03
333/106 = 3.1415094340 error 8.32e-05
355/113 = 3.1415929204 error 2.67e-07
103993/33102 = 3.1415926530 error 5.78e-10
104348/33215 = 3.1415926539 error 3.32e-10
355/113 is accurate to 7 digits because pi's next quotient (292) is huge --
a large partial quotient means the preceding convergent is exceptionally good.
Convergent error falls roughly as 1/denominator^2:
pi: 7:1.3e-03 106:8.3e-05 113:2.7e-07 33102:5.8e-10 33215:3.3e-10
e: 1:2.8e-01 3:5.2e-02 4:3.2e-02 7:4.0e-03 32:4.7e-04
wrote C:\Users\acwic\symplectic-nbody\examples\output\continued_fraction.svgThe Chinese Remainder Theorem: a number from its remainders
If you know a number's remainders modulo several pairwise-coprime moduli, the CHINESE REMAINDER THEOREM says there is a UNIQUE value modulo their product matching all of them, and constructs it. This ancient result (Sunzi, 3rd century) is the backbone of modern computing: RSA decryption speeds up about 4x by working modulo the prime factors separately and recombining with CRT; big-integer and polynomial arithmetic run in parallel residue systems; and it underlies secret sharing and error-correcting codes. It rests on the EXTENDED EUCLIDEAN ALGORITHM, which finds the Bezout identity a*x + b*y = gcd(a,b) and hence modular inverses. Given x = r_i (mod m_i) with coprime moduli, CRT builds x = sum r_i * M_i * (M_i^-1 mod m_i) mod M, each term hitting r_i in its own modulus and 0 in the others. When the moduli are not coprime the system is solvable only if the congruences agree on each pairwise gcd -- a generalized CRT that merges congruences and detects contradictions. This module implements extended Euclid, modular inverse, and both CRTs, verified against brute force: the solution satisfies every congruence and is the smallest non-negative one (matching exhaustive search over 200 systems), the Bezout identity is correct over 300 random pairs, non-coprime systems are solved when consistent and rejected when contradictory, and the classic Sunzi puzzle gives 23.
Chinese Remainder Theorem: a number from its remainders
Sunzi's classic puzzle (3rd century): a number leaves remainder
2 mod 3, 3 mod 5, 2 mod 7 -- what is it?
-> x = 23 (mod 105); check: 23%3=2, 23%5=3, 23%7=2
Reconstruct 8675309 from its residues mod [101, 103, 107, 109, 113]:
residues: [15, 31, 70, 108, 73]
CRT gives 8675309 (mod 13710311357); matches the secret: True
RSA speedup idea: instead of one exponentiation mod n = p*q,
do two mod p and mod q (each ~half the size, ~4x faster) and recombine with CRT.
value 123456789 recovered from (mod 1000003, mod 1000033): True
Non-coprime moduli (generalized CRT):
x = 3 mod 4 and x = 5 mod 6 -> 11 mod 12
x = 1 mod 2 and x = 0 mod 4 -> None (contradiction)
For pairwise-coprime moduli there's a unique answer mod their product, built as
sum(r_i * M_i * (M_i^-1 mod m_i)) where M_i = M/m_i -- each term hits r_i in its own
modulus and 0 in the others. The extended Euclidean algorithm supplies the inverses.
wrote C:\Users\acwic\symplectic-nbody\examples\output\crt.svgTonelli-Shanks: square roots modulo a prime
Solving x^2 = n (mod p) -- a MODULAR SQUARE ROOT -- is basic to number theory and cryptography: it decompresses elliptic-curve points (recovering y from x), appears in quadratic-sieve factoring and primality proving, and underlies the Rabin cryptosystem. There is no general formula, but the TONELLI-SHANKS algorithm finds a root in expected polynomial time whenever one exists. First, EULER'S CRITERION decides existence: n is a QUADRATIC RESIDUE mod p iff n^((p-1)/2) = 1, the LEGENDRE SYMBOL. If it is, and p = 3 mod 4, the root is simply n^((p+1)/4); otherwise Tonelli-Shanks writes p-1 = q*2^s, finds a quadratic non-residue, and iteratively squares and corrects a running candidate -- each step halving the order of the error -- until it squares to n. The two roots are r and p-r. This module implements the Legendre symbol, a residue test, and the modular square root, verified against brute force: the returned root squares back to n, a root is returned iff n is a genuine square (checked against the actual set of squares for every residue of 40 primes), both roots are r and p-r, the Legendre symbol matches a residue count with exactly (p-1)/2 residues per prime, and both the p = 3 mod 4 fast path and the general 1 mod 4 path give correct roots, up to million-scale primes.
Tonelli-Shanks: square roots modulo a prime
Modular square roots (x^2 = n mod p):
sqrt(10) mod 13: (6, 7) (check: 6^2 = 10 mod 13)
sqrt(2) mod 7: (3, 4) (check: 3^2 = 2 mod 7)
sqrt(5) mod 7: none (5 is a non-residue)
sqrt(123456) mod 1000033: (450092, 549941) (check: 450092^2 = 123456 mod 1000033)
Modulo 13: 6 residues [1, 3, 4, 9, 10, 12], 6 non-residues [2, 5, 6, 7, 8, 11]
(exactly (p-1)/2 = 6 of each, per the Legendre symbol)
Elliptic-curve point decompression on y^2 = x^3 + 2x + 3 (mod 1000003):
x = 5: no point (rhs 138 is a non-residue) -- would try another x
Euler's criterion (n^((p-1)/2) mod p) tells us if a root exists; if so, Tonelli-Shanks
finds it: for p = 3 mod 4 it's just n^((p+1)/4), otherwise it iteratively corrects a
candidate using a quadratic non-residue until the error's order drops to zero.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tonelli_shanks.svgBaby-step giant-step: the discrete logarithm in O(sqrt(n))
The DISCRETE LOGARITHM problem -- find x with g^x = h (mod m) -- is the hard problem underpinning Diffie-Hellman, ElGamal, and DSA, whose security rests on the belief that no fast general algorithm exists. Naive search is O(n) in the order of g; BABY-STEP GIANT-STEP cuts that to O(sqrt(n)) by meet-in-the-middle. Write x = i*N + j with N = ceil(sqrt(n)); then g^x = h becomes g^j = h*(g^{-N})^i. Precompute a hash table of the BABY STEPS g^j for all j, then take GIANT STEPS multiplying h by g^{-N} and looking each up -- a hit gives x = i*N + j. Both loops run sqrt(n) times, exponentially faster than brute force yet still exponential in the bit-length, which is why real cryptographic groups (256-bit and up) stay secure. This module implements BSGS modulo a prime and a multiplicative-order helper, verified against brute force: the returned x satisfies g^x = h, existence and validity agree with exhaustive search over dozens of primes, no-solution cases are reported, a toy Diffie-Hellman exchange is broken by recovering the secret exponent (and reconstructing the shared key), and the order helper matches known values.
Baby-step giant-step: discrete logarithm in O(sqrt(n))
Solving g^x = h (mod p):
3^x = 13 (mod 17) -> x = 4 (check: 3^4 = 13)
2^x = 22 (mod 29) -> x = 26 (check: 2^26 = 22)
7^x = 6 (mod 41) -> x = 39 (check: 7^39 = 6)
Breaking a toy Diffie-Hellman key exchange:
public: p=7919, g=7, A=1434, B=7138
(Alice and Bob's shared secret: 2002)
eavesdropper solves g^x = A -> x = 5555 (Alice's secret was 5555)
-> reconstructs the shared secret 2002: True
(this works only because p is tiny; real DH uses 2048+ bit primes)
Baby-step giant-step vs brute force (steps to solve):
p= 101: BSGS ~ 2*sqrt(n) = 22 steps vs brute force n = 100 steps
p= 1009: BSGS ~ 2*sqrt(n) = 64 steps vs brute force n = 1008 steps
p= 10007: BSGS ~ 2*sqrt(n) = 202 steps vs brute force n = 10006 steps
p= 100003: BSGS ~ 2*sqrt(n) = 634 steps vs brute force n = 100002 steps
Write x = i*N + j with N = ceil(sqrt(n)). Precompute the baby steps g^j in a hash map,
then take giant steps h*(g^-N)^i and look each up. A hit gives x = i*N + j. Both loops
run sqrt(n) times -- exponentially faster than brute force, but still exponential in bits.
wrote C:\Users\acwic\symplectic-nbody\examples\output\discrete_log.svgWelzl's algorithm: the smallest enclosing circle
Given a point cloud, the SMALLEST ENCLOSING CIRCLE is the circle of least radius containing all of them -- the tightest 'where is everything?' summary, the optimal facility placement minimizing the worst-case distance, and the bounding volume for collision culling. A key fact makes it tractable: the smallest circle is determined by at most THREE boundary points (two as a diameter, or three on a circumcircle). WELZL'S ALGORITHM finds it in EXPECTED LINEAR time by randomized incremental construction: process points in random order maintaining the current smallest circle; a point already inside is skipped, but a point outside MUST lie on the boundary of the new circle, so rebuild from the earlier points with it forced onto the boundary. With one, two, then three boundary points fixed, the circle is pinned down. This module implements the practical iterative (move-to-front) variant, returning the centre and radius, verified against brute force and exact references: every point lies inside, the radius matches an O(n^4) all-triples minimum over 60 random sets, shrinking the radius excludes a point (true minimality), interior points don't change it, and known cases hold (two points give a diameter, a square its circumscribed circle, points on a circle recover that circle) -- up to 1000-point sets.
Welzl's algorithm: the smallest circle enclosing a point cloud
50 points
smallest enclosing circle: centre (193.8, 164.2), radius 170.56
all points inside: True
support points (on the boundary): 2 -- the circle is pinned by 2 (a diameter)
For comparison, the bounding-box circumscribed circle has radius 186.03
-> the true minimum circle is 8% smaller in radius
The smallest circle is determined by at most 3 boundary points. Welzl processes points
in random order; a point inside the current circle is skipped, but one outside must lie
on the new circle's boundary, so the circle is rebuilt with it fixed there -- O(n) expected.
wrote C:\Users\acwic\symplectic-nbody\examples\output\welzl.svgCuckoo filters: approximate membership with deletion
A BLOOM FILTER answers 'have I seen this?' in tiny space but cannot DELETE items. The CUCKOO FILTER matches Bloom's space and false-positive rate, is often faster, AND supports deletion -- which is why it backs routers, databases, and deduplication that must remove items. Like Bloom it never gives a FALSE NEGATIVE; it only occasionally gives a false positive. It stores a small FINGERPRINT of each item in a table of buckets, with two candidate buckets per item found by CUCKOO HASHING: i2 = i1 XOR hash(fingerprint), the trick that lets either bucket recover the other from just the stored fingerprint, so lookups and deletes need only the fingerprint. Insertion places the fingerprint in either candidate; if both are full it evicts a random resident to ITS alternate bucket and repeats -- the 'cuckoo' kicking-out that reaches ~95% load. This module implements add, contains, and delete with configurable bucket size and fingerprint bits, verified that it never reports a false negative (every added, not-deleted item tests present), that the false-positive rate is small and shrinks with fingerprint size (0.067 at 4 bits down to ~0 at 16), that deletion removes an item while leaving others, that deleting one of several duplicates leaves the rest, that it packs to a ~95% load factor, and that it is reproducible from a seed.
Cuckoo filter: approximate membership with deletion
added 2000 items, load factor 0.24
no false negatives: True
false positives on 50000 absent items: 1 (rate 0.00002)
Deletion (impossible with a Bloom filter):
'user-500' present: True
after delete: False
'user-501' unaffected: True
False-positive rate shrinks as the fingerprint grows:
4 bits: false-positive rate 0.06656
6 bits: false-positive rate 0.03904
8 bits: false-positive rate 0.00082
10 bits: false-positive rate 0.00004
12 bits: false-positive rate 0.00004
16 bits: false-positive rate 0.00000
Each item stores a small fingerprint in one of two candidate buckets, the second found
by XORing the first with hash(fingerprint) -- so either bucket recovers the other from
the fingerprint alone. Full buckets kick out a resident to its alternate (the 'cuckoo').
wrote C:\Users\acwic\symplectic-nbody\examples\output\cuckoo_filter.svgRollback disjoint-set union: undoable connectivity
Ordinary UNION-FIND is nearly constant-time thanks to PATH COMPRESSION, but that same compression rewrites the tree unpredictably, so unions cannot be UNDONE. Offline dynamic connectivity needs exactly that: process edge additions and queries, then roll back to an earlier state. The trick is to drop path compression and use only UNION BY RANK, which changes O(1) state per union (one parent pointer, maybe one rank), recorded on a stack; rollback pops the stack and restores the saved values. This ROLLBACK DSU underlies the offline dynamic-connectivity structure (a segment tree over time), Kruskal-style reconnection, and 'components after each edge, later retracted' problems. Find is O(log n) (rank keeps trees shallow) rather than inverse-Ackermann, but every operation is reversible: a SNAPSHOT is just the stack length. This module implements union by rank, find, a component counter, and snapshot/rollback, verified against a brute-force recompute: connectivity queries always match a fresh union-find over the live edges (50 random graphs), rolling back to a snapshot exactly restores connectivity and the component count, nested snapshots roll back correctly, a full add-then-rollback returns to all-singletons, redundant unions roll back cleanly, and a dynamic-connectivity scenario with edge retraction behaves correctly.
Rollback disjoint-set union: undoable connectivity
8 isolated nodes -> 8 components
Adding edges (recording a snapshot before each):
edge 0-1: merged -> 7 components
edge 2-3: merged -> 6 components
edge 4-5: merged -> 5 components
edge 1-2: merged -> 4 components
edge 6-7: merged -> 3 components
edge 3-4: merged -> 2 components
Now 2 components; 0-5 chained together, 6-7 a separate pair
Roll back 3 edges (undo 1-2, 6-7, 3-4):
components: 5
0 and 3 connected: False (retracted -- the 1-2 bridge is gone)
0 and 1 connected: True (kept -- it was before the snapshot)
Full rollback to the start: 8 components (back to all singletons: True)
Ordinary union-find uses path compression and cannot be undone. Rollback DSU uses only
union by rank -- each union changes O(1) state (one parent, maybe one rank) -- and records
those changes on a stack, so a snapshot is a stack length and rollback replays it in reverse.
wrote C:\Users\acwic\symplectic-nbody\examples\output\dsu_rollback.svgWeighted interval scheduling: the most valuable non-overlapping jobs
Given jobs with a start, finish, and WEIGHT, pick a subset of mutually non-overlapping jobs maximizing total weight -- booking a resource for the most valuable non-conflicting requests. With equal weights this is the classic activity-selection, solved by an earliest-finish greedy, but arbitrary weights defeat greed and need DYNAMIC PROGRAMMING. Sort jobs by finish time; for job i let p(i) be the latest job finishing at or before i starts (binary search); then opt(i) = max(opt(i-1), weight_i + opt(p(i))) -- skip job i or take it plus the best schedule among jobs finishing before it begins. Filling opt is O(n log n) and tracing back the max choices recovers the chosen set. This module implements the weighted DP (with the selected jobs) and the greedy count-maximizing version, verified against brute force over all 2^n subsets that the DP finds the true maximum weight, the returned jobs are genuinely non-overlapping and sum to that weight, the greedy count matches the maximum independent set of intervals, equal weights make the DP agree with the greedy count, and a high-value long job correctly beats several small ones. In the demo the DP's optimum of 60 beats both a count-maximizing greedy (50) and a highest-value-first greedy (40).
Weighted interval scheduling: the most valuable non-conflicting jobs
Requests (start, finish, value):
A: [0, 3) value 20
B: [1, 4) value 5
C: [3, 5) value 10
D: [4, 7) value 25
E: [5, 9) value 30
F: [6, 8) value 8
G: [8, 10) value 12
H: [2, 9) value 40
Optimal schedule (weighted DP): value 60.0, jobs ['A', 'C', 'E']
Count-maximizing greedy: 4 jobs ['A', 'C', 'F', 'G'], value 50
Greedy highest-value-first: value 40 (greedy is not optimal in general)
-> the DP's 60.0 beats both greedies; only dynamic programming guarantees the optimum.
Sort by finish time; for each job find p(i), the latest job finishing before it starts
(binary search). Then opt(i) = max(skip job i, value_i + opt(p(i))). O(n log n), and
tracing back the max choices recovers the chosen set. Equal weights reduce to the greedy.
wrote C:\Users\acwic\symplectic-nbody\examples\output\interval_scheduling.svgCoin change: fewest coins, ways to make change, and the greedy trap
Given coin denominations and a target, two classic questions: the MINIMUM number of coins summing to the amount, and the NUMBER OF WAYS to make it. Both are textbook DYNAMIC PROGRAMMING, and both expose a trap: the obvious GREEDY (take the largest coin that fits) is right for canonical currency like US coins but FAILS for many denomination sets -- with {1, 3, 4} making 6, greedy gives 4+1+1 (3 coins) while the optimum is 3+3 (2 coins). Only DP is guaranteed correct. The minimum-coins DP builds min[a] = 1 + min over coins c of min[a-c], with recorded choices reconstructing the coin multiset. The counting DP is subtler: iterating coins in the OUTER loop counts order-independent COMBINATIONS, while the amount outer counts ordered SEQUENCES. This module computes the minimum coins (with the coins used), makeability, and both counts, verified against brute force: the minimum is truly minimal and the reconstructed coins sum to the amount (150 random instances), unmakeable amounts are detected, the combination count matches a brute enumeration, greedy-defeating sets are handled ({1,3,4}->6 gives 2, {1,15,25}->30 gives 2 vs greedy's 6), and canonical currency agrees with greedy.
Coin change: fewest coins, number of ways, and the greedy trap
US currency {1, 5, 10, 25} (greedy works here):
63c -> 6 coins [1, 1, 1, 10, 25, 25]
99c -> 9 coins [1, 1, 1, 1, 10, 10, 25, 25, 25]
41c -> 4 coins [1, 5, 10, 25]
The greedy trap -- coins {1, 3, 4}, make 6:
DP optimum: 2 coins [3, 3]
greedy (largest-first): 3 coins (4 + 1 + 1) -- WRONG, one more coin than needed
Another greedy failure -- coins {1, 15, 25}, make 30:
DP: 2 coins [15, 15]; greedy: 6 coins (25 + five 1s)
Number of ways to make change (combinations vs ordered sequences):
5 with {1,2,5}: 4 combinations, 9 ordered sequences
10 with {1,2,5}: 10 combinations, 128 ordered sequences
The minimum-coins DP builds min[a] = 1 + min over coins c of min[a-c]. The counting DP
puts coins in the OUTER loop (combinations) or the amount outer (sequences). Greedy is
only correct for 'canonical' coin systems; DP is always right.
wrote C:\Users\acwic\symplectic-nbody\examples\output\coin_change.svgCombinatorial ranking: objects as integers
Every finite combinatorial object -- a permutation of n items, a k-subset of n, a bit-string -- can be given a unique integer RANK from 0 to (count-1), and recovered from it by UNRANKING. This bijection is the engine of combinatorial generation: store a permutation as one integer, draw a uniformly random one by ranking a random integer, or split an enumeration across machines by rank range -- all without ever building the (often astronomically large) full set. Permutations use the FACTORIAL NUMBER SYSTEM: the Lehmer code counts, at each position, how many not-yet-used smaller elements remain, and those digits times descending factorials give the lexicographic rank. Combinations use the COMBINATORIAL NUMBER SYSTEM, a mixed-radix representation in binomial coefficients. Gray code ranks bit-strings so consecutive ranks differ in exactly one bit (rank = n XOR n>>1), the reflected binary code used in rotary encoders to avoid transition glitches. All exact integer arithmetic, so unranking jumps straight to the trillionth permutation of 15 elements. Verified against brute force: ranking then unranking is the identity, ranks form a contiguous 0..N-1 bijection with no gaps or collisions, permutation ranks match Python's lexicographic itertools order, combination ranks match itertools.combinations, and consecutive Gray codes differ in exactly one bit.
Combinatorial ranking: objects <-> integers
Permutations of {0,1,2,3} (lexicographic rank):
rank 0 <-> [0, 1, 2, 3] (rank back: 0)
rank 5 <-> [0, 3, 2, 1] (rank back: 5)
rank 12 <-> [2, 0, 1, 3] (rank back: 12)
rank 23 <-> [3, 2, 1, 0] (rank back: 23)
Combinations: 3-subsets of {0..5} (combinatorial number system):
rank 0 <-> [0, 1, 2] (rank back: 0)
rank 5 <-> [0, 2, 4] (rank back: 5)
rank 10 <-> [1, 2, 3] (rank back: 10)
rank 19 <-> [3, 4, 5] (rank back: 19)
Unranking WITHOUT enumeration -- the trillionth permutation of 15 elements:
permutation #1,000,000,000,000 of 15!: [11, 6, 8, 10, 1, 2, 14, 4, 5, 13, 3, 9, 12, 0, 7]
(15! = 1,307,674,368,000; we jump straight to it, no listing)
Gray code -- consecutive integers whose codes differ in one bit:
n: [0, 1, 2, 3, 4, 5, 6, 7]
gray: [0, 1, 3, 2, 6, 7, 5, 4]
binary: ['000', '001', '011', '010', '110', '111', '101', '100']
each adjacent pair flips exactly one bit -- used in rotary encoders to avoid glitches.
A rank is a unique 0..N-1 index for each object; unranking recovers it. This lets you
store a permutation as one integer, pick a uniformly random one, or split an enumeration
across machines by rank range -- all without materializing the (huge) full set.
wrote C:\Users\acwic\symplectic-nbody\examples\output\combinatorial_rank.svgBridges and articulation points: single points of failure
In an undirected network a BRIDGE is an edge whose removal disconnects the graph and an ARTICULATION POINT (cut vertex) is a vertex whose removal does -- the structural weak points where one cut severs the network. Finding them is the first question of reliability analysis: a graph with none is 2-connected and survives any single failure. The naive test deletes each edge or vertex and recounts components, O(V*(V+E)). Tarjan's algorithm finds them all in ONE depth-first pass, O(V+E), using the discovery time disc[u] (stamped when DFS first reaches u) and the low-link low[u] (smallest disc reachable from u's subtree via tree edges plus one back edge). A tree edge (u,v) is a bridge exactly when low[v] > disc[u] -- v's subtree has no back edge climbing past u; a non-root u is a cut vertex when some child has low[v] >= disc[u], and the DFS root when it has two or more children. Parallel edges must be skipped by edge-id, not vertex, so a doubled link is never falsely called a bridge. This module finds all bridges and cut vertices in one iterative DFS (no recursion, safe on a 5000-deep path) and reports the 2-edge-connected components left when every bridge is cut. Verified against the definition on 400 random graphs: an edge is a bridge iff deleting it raises the component count, a vertex is a cut vertex iff deleting it does; trees have every edge a bridge, cycles and K5 have neither.
Bridges and articulation points: a 10-node, 3-cluster network
10 nodes, 13 edges, 3 clusters joined by thin links
Bridges (edge whose loss splits the network): 3
2 -- 3
5 -- 6
8 -- 9
Articulation points (node whose loss splits the network): [2, 3, 5, 6, 8]
2-edge-connected components (groups that survive any single link failure):
group 0: [0, 1, 2]
group 1: [3, 4, 5]
group 2: [6, 7, 8]
group 3: [9]
Reliability reading: every bridge and cut vertex is a single point of failure. The
dense cluster B (with its extra 3--5 chord) has no internal bridge, but the two links
wiring the clusters together (2--3, 5--6) and the leaf link (8--9) are all bridges --
cut any one and the network splits. Tarjan finds them in one O(V+E) DFS pass; the naive
check would delete each edge and recount components, O(V*(V+E)).
wrote C:\Users\acwic\symplectic-nbody\examples\output\bridges.svgMaximum bipartite matching: pairing two sides
A bipartite graph splits its vertices into two sides -- workers and jobs, students and projects, riders and taxis -- with edges only between them. A MATCHING is a set of edges sharing no vertex; the MAXIMUM matching pairs as many as possible. Unlike the Hungarian algorithm (which minimises total COST on a weighted complete graph), this is the unweighted question -- maximise the COUNT -- on an arbitrary graph where only some pairs are allowed. The engine is the AUGMENTING PATH: an alternating unmatched/matched path between two free vertices, which when flipped raises the matching by one; Berge's theorem says a matching is maximum exactly when none remains. HOPCROFT-KARP runs a BFS to find the shortest augmenting-path length, then a DFS to pack many vertex-disjoint shortest paths and flip them together -- only O(sqrt(V)) phases, so O(E*sqrt(V)) overall versus O(V*E) for one path at a time. The result ties to two classics: KONIG'S THEOREM (max matching == minimum vertex cover in a bipartite graph, recovered from alternating-reachability) and HALL'S THEOREM (a left-perfect matching exists iff every subset S of the left has at least |S| neighbours). This module computes the maximum matching, the minimum vertex cover, and the maximum independent set. Verified against brute force on 500 random graphs (the size is truly maximum), against Konig (the cover size equals the matching and touches every edge), and against Hall (perfect-left iff the subset condition), plus a 2000x2000 sparse instance.
Maximum bipartite matching: staffing 5 workers onto 5 jobs
Qualifications:
Ada -> frontend, backend
Ben -> backend, data
Cam -> data, ops
Dee -> frontend, design
Eli -> design
Maximum placement: 5 of 5 workers assigned
Ada -> backend
Ben -> data
Cam -> ops
Dee -> frontend
Eli -> design
Perfect placement possible? True
Konig minimum cover (5 = matching size): ['Ada', 'Ben', 'Cam', 'Dee', 'Eli']
Every qualification edge touches at least one of these -- the smallest such set, and by
Konig's theorem its size equals the maximum matching. Hopcroft-Karp finds the matching
in O(E*sqrt(V)) by augmenting many shortest paths per BFS phase, not one at a time.
wrote C:\Users\acwic\symplectic-nbody\examples\output\bipartite_matching.svgMinimum-cost maximum flow: cheapest way to push the most
A flow network's edges each carry a CAPACITY and now a COST PER UNIT. Plain max-flow asks how much can be pushed source-to-sink; MINIMUM-COST MAXIMUM FLOW asks for the cheapest way to push that maximum. It is the workhorse of operations research -- route goods factory-to-store minimising shipping, assign workers minimising cost, schedule for maximal throughput at minimal expense -- and it strictly generalises both plain max-flow (all costs zero) and the assignment problem (a unit-capacity bipartite network), so one solver answers all three. The method is SUCCESSIVE SHORTEST PATHS: repeatedly find the cheapest source-to-sink path in the RESIDUAL graph (where each edge of cost c gains a reverse arc of cost -c that lets flow be cancelled) and push as much as the bottleneck allows. Because every augmentation follows a minimum-cost path, the running cost stays minimal, and when none remains the flow is both maximum and cheapest. The negative reverse-arc costs mean Dijkstra alone won't do; this uses SPFA (a queue-based Bellman-Ford), keeping it simple and dependency-free. Verified against independent references: the flow VALUE equals the Edmonds-Karp max-flow (200 random nets), the COST is confirmed minimal by brute force over all integer flows (120 tiny nets), and built as a bipartite assignment network its optimum matches the Hungarian algorithm and the best over all permutations.
Minimum-cost maximum flow: a factory-to-store shipping network
Maximum shippable: 7 units, at minimum total cost 22
Flow on each pipe (used / capacity, unit cost):
factA -> whX 3/3 @ 2/unit = 6
factA -> whY 0/2 @ 4/unit = 0
factB -> whX 1/2 @ 3/unit = 3
factB -> whY 3/3 @ 1/unit = 3
whX -> hub 4/4 @ 1/unit = 4
whY -> hub 3/4 @ 2/unit = 6
Successive shortest paths: each round routes flow along the cheapest remaining path in
the residual graph (with negative-cost reverse arcs handled by SPFA), so the running
cost is always minimal for the flow sent. When no path remains the flow is both maximum
and cheapest -- generalising plain max-flow (zero costs) and the assignment problem.
Same engine, assignment problem: 3 workers -> 3 jobs, min cost 9
worker->job: [1, 0, 2] (worker i takes job assign[i])
wrote C:\Users\acwic\symplectic-nbody\examples\output\min_cost_flow.svgSprague-Grundy: every impartial game is secretly Nim
An IMPARTIAL game -- two players, perfect information, identical moves for both, last to move wins -- has a stunning universal structure. The Sprague-Grundy theorem says every position is equivalent to a single Nim heap: it carries a GRUNDY NUMBER (nimber), and a position is a LOSS for the player to move exactly when that number is 0. The nimber comes from the MEX rule: g(position) = mex{ g(p) : p reachable in one move }, where mex is the smallest non-negative integer absent from the set; a terminal position with no moves has mex{} = 0, a loss for whoever cannot move. The second miracle is COMPOSITION -- when a game splits into independent subgames (several heaps, several rows), the whole's Grundy number is the XOR (Nim-sum) of the parts, so who-wins-a-sum-of-games reduces to XOR-ing small integers, and the winning move is the one making the total Nim-sum zero. This module gives the mex operator, a memoised Grundy solver for any impartial game (supplied as a 'moves from a position' function), the Nim-sum combinator, and three worked games: Nim, subtraction games, and Kayles. Verified against a brute-force minimax oracle (Grundy==0 iff the position is a theoretical loss, on Nim, subtraction, and Kayles) and against known results: Nim's XOR-of-heaps rule, the periodicity of subtraction-game nimbers (remove {1,2,3} gives Grundy = n mod 4), and the published Kayles sequence.
Sprague-Grundy: every impartial game is a Nim heap of size = its Grundy number
Nim heaps (3, 4, 5): Grundy = 2 (= 3 XOR 4 XOR 5). Nonzero -> a WIN for the mover.
Winning moves (leave a Grundy-0 position, i.e. Nim-sum 0):
move to (1, 4, 5)
Nim heaps (1, 2, 3): Grundy = 0 -> a LOSS for the mover; no move escapes.
Subtraction game, remove [1, 2, 3] stones: Grundy(n) for n=0..15:
[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
-> periodic with period 4 (Grundy = n mod 4); losing heaps are multiples of 4.
Kayles (knock down 1 or 2 adjacent pins from a row): Grundy(row of n) for n=0..12:
[0, 1, 2, 3, 1, 4, 3, 2, 1, 4, 2, 6, 4]
-> the famous irregular Kayles nimbers, eventually periodic with period 12.
Composition: a position with a Nim heap of 5 and a Kayles row of 4 has Grundy
5 XOR 1 = 4. Combine any games by XOR-ing nimbers.
wrote C:\Users\acwic\symplectic-nbody\examples\output\sprague_grundy.svgFast Walsh-Hadamard transform: the FFT for XOR
The FFT multiplies polynomials, which is CYCLIC convolution -- combining sequences by adding indices mod n. A different convolution combines indices by bitwise XOR: (a*b)[k] = sum over i XOR j == k of a[i]*b[j]. This answers questions like the distribution of the XOR of two independent random bitmasks, or -- in game theory -- the Nim-sum distribution of a combined game. Direct computation is O(n^2); the FAST WALSH-HADAMARD TRANSFORM does it in O(n log n), exactly as the FFT speeds up cyclic convolution. The FWHT is a butterfly almost identical to the FFT's: at each of the log2(n) stages, pair entries a distance h apart and replace (x,y) with (x+y, x-y). By the CONVOLUTION THEOREM, transforming both inputs, multiplying pointwise, and inverse-transforming yields their XOR convolution -- because the Hadamard matrix diagonalises the XOR group algebra. Two relatives over the same bit lattice give the OR convolution (via the sum-over-subsets zeta transform and its Mobius inverse) and the AND convolution (via the superset-sum transform), covering all three Boolean operations. Everything is integer-exact -- no floating-point error. Verified against the brute-force O(n^2) definition of each convolution on hundreds of random arrays, against the round-trip identity, linearity, commutativity, the delta identity, and a 2^16-point transform.
Fast Walsh-Hadamard transform: the FFT for XOR
Two independent processes each emit a 3-bit value with these weights:
A (sum 12): [4, 3, 2, 1, 1, 1, 0, 0]
B (sum 12): [0, 0, 1, 2, 3, 3, 2, 1]
Distribution of the COMBINED value under each bitwise operation:
value: [0, 1, 2, 3, 4, 5, 6, 7]
A XOR B: [10, 11, 13, 14, 26, 25, 23, 22]
A OR B: [0, 0, 6, 24, 15, 39, 21, 39]
A AND B: [85, 23, 15, 3, 14, 4, 0, 0]
Each total mass = 12*12 = 144: XOR 144, OR 144, AND 144 -- all conserved.
All three match the brute-force O(n^2) definition exactly (integer, no rounding).
The FWHT computes XOR convolution in O(n log n) by transforming both inputs with the
Hadamard butterfly (x,y)->(x+y,x-y), multiplying pointwise, and inverse-transforming --
the same convolution theorem that makes the FFT fast, but for the XOR group. OR and AND
convolutions use the subset-sum (zeta) and superset-sum transforms over the bit lattice.
wrote C:\Users\acwic\symplectic-nbody\examples\output\walsh_hadamard.svgDPLL: deciding Boolean satisfiability
SATISFIABILITY (SAT) asks whether a formula in conjunctive normal form -- an AND of clauses, each an OR of literals -- can be made true. It is the archetypal NP-complete problem: circuit verification, planning, dependency resolution, and countless puzzles compile down to it. Brute force tries all 2^n assignments; DPLL (Davis-Putnam-Logemann-Loveland, 1962) is the backtracking search at the heart of every modern SAT solver and in practice explores a tiny fraction of that space. It is depth-first assignment with two pruning rules. UNIT PROPAGATION: a clause with all but one literal false forces that literal true, cascading through the formula; a clause with every literal false is a conflict, killing the branch. PURE LITERAL: a variable appearing with only one polarity is fixed that way for free. After propagation DPLL picks a variable, tries true, recurses, and on failure tries false. This module parses a CNF (clauses of signed integers), decides satisfiability with unit propagation and pure-literal elimination, and returns a satisfying model. Verified against a brute-force truth-table oracle on 600 random formulas (SAT exactly when some assignment works, every returned model genuinely satisfies all clauses) and on the pigeonhole principle -- n+1 pigeons into n holes is proven UNSAT without enumerating the 2^n space.
DPLL: Boolean satisfiability by backtracking with propagation
Formula (CNF, AND of ORs):
(x1 v x2 v ~x3) ^ (~x1 v x3) ^ (~x2 v x3) ^ (x1 v ~x2) ^ (x2 v x3)
DPLL: SATISFIABLE
model: x1=T, x2=T, x3=T
verified: True
Pigeonhole principle -- can n+1 pigeons occupy n holes, no two sharing?
2 pigeons, 1 holes: 2 vars, 3 clauses -> UNSAT (proven impossible)
3 pigeons, 2 holes: 6 vars, 9 clauses -> UNSAT (proven impossible)
4 pigeons, 3 holes: 12 vars, 22 clauses -> UNSAT (proven impossible)
5 pigeons, 4 holes: 20 vars, 45 clauses -> UNSAT (proven impossible)
Every case UNSAT -- DPLL proves the combinatorial impossibility by exhausting the
(heavily pruned) search tree, never enumerating all 2^n assignments.
Unit propagation on a forcing chain x1 -> x2 -> ... -> x5:
x1=T x2=T x3=T x4=T x5=T -- one forced unit cascades through the whole chain.
wrote C:\Users\acwic\symplectic-nbody\examples\output\dpll.svgBerlekamp-Massey: the recurrence hidden in a sequence
Many sequences obey a LINEAR RECURRENCE -- each term a fixed combination of the previous few, s[n] = c1*s[n-1] + ... + cL*s[n-L]. Fibonacci does (s[n]=s[n-1]+s[n-2]); a linear-feedback shift register (LFSR), the workhorse of stream ciphers and CRCs, is exactly such a recurrence over GF(2). Berlekamp-Massey solves the inverse problem: given only the first terms, find the SHORTEST recurrence that produces them. Its length L (the LINEAR COMPLEXITY) measures predictability -- a short recurrence pins down the whole infinite sequence from a handful of coefficients. The algorithm scans left to right keeping a recurrence that predicts every term so far; at each step it computes the DISCREPANCY with the actual next term and, if nonzero, corrects the recurrence by adding a scaled shift of the best previous failed one -- a construction that provably stays minimal, in O(n^2). This module works over the exact rationals (Fibonacci, Tribonacci, Pell all handled with no rounding) and over GF(2) for the binary LFSR case. Verified by round-trip (the recovered recurrence regenerates the input and is never longer than the true generator, on 300 random recurrences), against a brute-force minimality search, and on GF(2) m-sequences whose complexity is recovered from just 2L bits -- the classic reason a raw LFSR keystream is cryptographically broken.
Berlekamp-Massey: find the shortest linear recurrence behind a sequence
Fibonacci complexity 2: s[n] = 1*s[n-1] + 1*s[n-2]
next 3 terms: [55, 89, 144]
Tribonacci complexity 3: s[n] = 1*s[n-1] + 1*s[n-2] + 1*s[n-3]
next 3 terms: [81, 149, 274]
Pell complexity 2: s[n] = 2*s[n-1] + 1*s[n-2]
next 3 terms: [408, 985, 2378]
powers of 2 complexity 1: s[n] = 2*s[n-1]
next 3 terms: [128, 256, 512]
Jacobsthal complexity 2: s[n] = 1*s[n-1] + 2*s[n-2]
next 3 terms: [85, 171, 341]
Linear-complexity attack on a stream cipher (LFSR keystream):
true LFSR: length 5, taps [0, 0, 1, 0, 1]
observed keystream: 10011111000110111010...
from 6 bits: recovered complexity 3, predicts full stream: False
from 8 bits: recovered complexity 4, predicts full stream: False
from 10 bits: recovered complexity 5, predicts full stream: True
from 12 bits: recovered complexity 5, predicts full stream: True
Once you see 2L bits, Berlekamp-Massey recovers the whole register and predicts
every future bit -- why a raw LFSR is cryptographically useless despite a long period.
wrote C:\Users\acwic\symplectic-nbody\examples\output\berlekamp_massey.svgSuffix automaton: one tiny machine, every substring
A SUFFIX AUTOMATON is the smallest deterministic automaton that recognises exactly the suffixes of a string -- and whose paths from the start state spell every DISTINCT SUBSTRING. It is astonishingly compact: at most 2n-1 states and 3n-4 transitions for a length-n string, yet it encodes all of the up-to n(n+1)/2 substrings. That makes it the Swiss-army knife of string processing: count distinct substrings, test a pattern in O(pattern), count how many times each substring occurs, find the longest repeated substring, and compute the longest common substring of two strings -- all in linear or near-linear time. It is built ONLINE in amortised O(n): each state is an equivalence class of substrings sharing the same set of end positions, the states form a tree under SUFFIX LINKS (the suffix tree of the reversed string), and appending a character follows those links, CLONING a state when a transition would otherwise conflict -- the trick that keeps the machine minimal. Distinct substrings then equal the sum over states of len[v]-len[link[v]]. Verified against brute force: the distinct-substring count matches the set of all O(n^2) substrings, membership and occurrence counts match direct scanning, and the longest common substring matches an O(n*m) DP -- on hundreds of random strings, with the state count confirmed within the linear 2n bound.
Suffix automaton: the smallest machine recognising every substring
word: 'abracadabra' (11 characters)
automaton: 12 states (bound is 2n-1 = 21)
distinct substrings: 54 (vs 66 raw substrings with repeats)
verified against brute enumeration: True
occurrence counts:
'abra' appears 2 time(s)
'a' appears 5 time(s)
'ra' appears 2 time(s)
'cad' appears 1 time(s)
longest repeated substring: 'abra'
longest common substring of 'abracadabra' and 'cadabraxyz': 'cadabra'
Built online in O(n): each character is appended by following suffix links and cloning
a state when a transition would conflict. The result has at most 2n-1 states yet encodes
all substrings -- every path from the start spells a distinct one. It answers substring
membership in O(pattern), counts occurrences, and finds longest common/repeated pieces.
wrote C:\Users\acwic\symplectic-nbody\examples\output\suffix_automaton.svgLyndon words: the primes of strings
A LYNDON WORD is a string strictly smaller (dictionary order) than all of its rotations -- 'aab' beats 'aba' and 'baa', so it is Lyndon; 'aba' is not, since its rotation 'aab' is smaller. Lyndon words are the primes of concatenation: the CHEN-FOX-LYNDON theorem says every string factorises UNIQUELY into a non-increasing sequence of Lyndon words w1 >= w2 >= ... >= wk. This underlies the Lyndon basis of free Lie algebras, the linear-time bijective Burrows-Wheeler transform, and -- through the last factor -- the LEXICOGRAPHICALLY SMALLEST ROTATION of a string (necklace canonicalisation, Booth's problem). DUVAL'S ALGORITHM computes the factorisation in O(n) time and O(1) space with a two-pointer scan: compare each character to the one a period back, extending the period on a tie, restarting on a larger character, and emitting Lyndon words on a smaller one; run over the doubled string it yields the least rotation. The FKM algorithm generates all Lyndon words up to a length in lexicographic order, and concatenating those whose length divides n builds the De Bruijn sequence. Verified against brute force: the factorisation's parts are all Lyndon and non-increasing and concatenate back, membership matches the rotation definition, the least rotation matches an exhaustive scan, and the generated words match both a brute filter and the Mobius necklace-counting formula -- on hundreds of random cases.
Lyndon words: the primes of string concatenation
A Lyndon word is strictly smaller than all its rotations. Membership:
aab -> Lyndon
aba -> not Lyndon
abcab -> not Lyndon
aabb -> Lyndon
z -> Lyndon
aa -> not Lyndon
Chen-Fox-Lyndon factorisation (unique, non-increasing) via Duval O(n):
banana = b | an | an | a
bbababaab = b | b | ab | ab | aab
abcabcabc = abc | abc | abc
zyxabc = z | y | x | abc
Least rotation (necklace canonicalisation) via Duval over the doubled string:
cabab -> 'ababc' (rotate by 1)
bca -> 'abc' (rotate by 2)
googgle -> 'egooggl' (rotate by 6)
tobeornottobe -> 'beornottobeto' (rotate by 2)
Lyndon words over {a,b} up to length 4 (FKM order -- concatenated they form the
De Bruijn sequence B(2,4)):
['a', 'aaab', 'aab', 'aabb', 'ab', 'abb', 'abbb', 'b']
De Bruijn B(2,4) = aaaabaabbababbbb (length 16 = 2^4)
wrote C:\Users\acwic\symplectic-nbody\examples\output\lyndon.svgEertree: every distinct palindrome in linear space
A string of length n contains at most n distinct palindromic substrings (a classical bound), and the EERTREE, or palindromic tree, stores ALL of them in O(n) space, built in O(n) time. Invented by Mikhail Rubinchik in 2014, it is the palindrome analogue of the suffix automaton: where that captures every substring, the eertree captures exactly the distinct palindromic ones, answering 'how many distinct palindromes?', 'how many end at each position?', and 'how often does each occur?' in linear time -- questions that arise in bioinformatics (palindromic DNA marks restriction sites and hairpins) and text indexing. The tree has two roots (an imaginary root of length -1 and the empty root of length 0) and one node per distinct palindrome, each with its length, a SUFFIX LINK to its longest proper palindromic suffix, and character edges where edge c from node v leads to the palindrome c+v+c. Building is online: to add a character the algorithm walks suffix links from the last palindrome to the longest one that can be extended by it, creating a node if new -- and the total link-walking is amortised O(n). Verified against brute force -- the distinct count and the per-length set match all O(n^2) substrings filtered for the palindrome property, and occurrence counts match direct scanning -- on hundreds of random strings, plus the classical <= n bound and that the online build equals batch construction.
Eertree: all distinct palindromic substrings in one linear structure
word: 'abacabadabacaba' (15 characters)
tree: 17 nodes (2 roots + one per distinct palindrome)
distinct palindromic substrings: 15
verified against brute enumeration: True
classical bound (<= n = 15): satisfied
distinct palindromes by length:
len 1: ['a', 'b', 'c', 'd']
len 3: ['aba', 'aca', 'ada']
len 5: ['bacab', 'badab']
len 7: ['abacaba', 'abadaba']
len 9: ['cabadabac']
len 11: ['acabadabaca']
len 13: ['bacabadabacab']
len 15: ['abacabadabacaba']
most frequent palindromes:
'a' occurs 8 times
'b' occurs 4 times
'aba' occurs 4 times
'c' occurs 2 times
'aca' occurs 2 times
palindromic richness: 15/15 distinct -> RICH (maximal)
The eertree adds each character in amortised O(1) by walking suffix links to the longest
extendable palindromic suffix -- capturing all palindromes in O(n), the palindrome
analogue of the suffix automaton.
wrote C:\Users\acwic\symplectic-nbody\examples\output\eertree.svgLi Chao tree: the lower envelope of a bundle of lines
Many optimisation problems reduce to one primitive: keep a growing set of lines y = m*x + b and repeatedly ask for the minimum (or maximum) value at a given x. As x sweeps, the answer traces the LOWER ENVELOPE -- a piecewise-linear convex curve. This is the query behind the CONVEX HULL TRICK, which collapses a large family of O(n^2) dynamic programs (dp[i] = min over j of dp[j] + cost(j,i) with cost linear in i) to O(n log n): each transition is a line, each state a query. The LI CHAO TREE supports it cleanly when lines arrive in ARBITRARY order with queries interleaved -- the case the classic monotonic-stack hull trick cannot handle. It is a segment tree over the x-domain; each node owns the line minimal at its midpoint. Inserting compares the new line to the node's at the midpoint, keeps the lower, and recurses into the half where the loser might still win -- since two lines cross at most once, that is O(log range). A query takes the minimum of every line on its root-to-leaf path, also O(log range). Verified against brute force: every min and max query matches the true optimum over all inserted lines, on hundreds of random line sets and points, including interleaved insert/query order and a convex-hull-trick DP matching its O(n^2) reference.
Li Chao tree: minimum of a set of lines at any x, in O(log range)
6 lines inserted (in arbitrary order):
y = -2x + 40
y = -1x + 20
y = +0x + 8
y = +1x + 2
y = +2x + 6
y = +3x + 30
lower-envelope minimum across x:
x min y which line
-12 -18.0 y=+2x+6
-9 -12.0 y=+2x+6
-6 -6.0 y=+2x+6
-3 -1.0 y=+1x+2
0 2.0 y=+1x+2
3 5.0 y=+1x+2
6 8.0 y=+0x+8
9 8.0 y=+0x+8
12 8.0 y=-1x+20
matches brute-force min over all lines at every integer x: True
Each line is inserted by comparing it to the node's line at the interval midpoint,
keeping the lower one and pushing the other into the half where it might still win --
O(log range) per insert and per query, and unlike the monotonic-stack convex hull trick,
it handles lines arriving in ANY order with queries interleaved. This is the standard
speed-up that turns an O(n^2) DP with linear transition costs into O(n log n).
wrote C:\Users\acwic\symplectic-nbody\examples\output\li_chao.svgMo's algorithm: batch range queries by reordering
Given a fixed array and a batch of range queries -- 'how many DISTINCT values in [l, r]?', 'what is the sum of squared frequencies?' -- the naive approach recomputes each in O(n), for O(q*n) total. MO'S ALGORITHM answers all q queries OFFLINE in O((n+q)*sqrt(n)) by maintaining a current window and a running answer, and MOVING the endpoints one element at a time (each an O(1) add/remove) to morph one query's range into the next. The cost is the total distance the two pointers travel, and the trick is an ORDER that minimises it: sort queries into sqrt(n)-sized blocks by left endpoint, then by right endpoint with the direction alternating per block (a snake order), so the left pointer moves O(sqrt(n)) per query and the right O(n) per block. The problem need only supply a cheap add and remove that update the running answer -- a frequency table and a nonzero-count for distinct values, an incremental sum(f^2) for the power sum. Because it permutes the queries it is inherently offline. Verified against brute force: every distinct-count and power-sum answer matches a direct recomputation over the subrange, on hundreds of random arrays and query batches plus a 2000x2000 stress test, with answers returned in the original query order.
Mo's algorithm: many range queries in O((n+q) sqrt n) by clever ordering
array (20 elements): [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3, 8, 4]
range distinct power-sum
[ 0, 5] 5 8
[ 3, 9] 6 9
[10,15] 5 8
[ 0,19] 9 52
[ 5,12] 6 12
[14,19] 5 8
[ 2, 8] 6 9
[ 7,11] 4 7
distinct counts match brute force: True
block size = sqrt(20) = 4
query order chosen (by left-block, then snaking right endpoint):
query 0: [0,5] (block 0)
query 6: [2,8] (block 0)
query 1: [3,9] (block 0)
query 3: [0,19] (block 0)
query 4: [5,12] (block 1)
query 7: [7,11] (block 1)
query 2: [10,15] (block 2)
query 5: [14,19] (block 3)
total pointer travel -- Mo's order: 56, original order: 99
Reordering shrinks the distance the window's two ends must travel; each step adds or
removes one element in O(1), so total time is the total travel, O((n+q) sqrt n).
wrote C:\Users\acwic\symplectic-nbody\examples\output\mo_algorithm.svgChu-Liu/Edmonds: the directed minimum spanning tree
The minimum spanning tree connects an undirected graph at least cost; its directed cousin is the MINIMUM SPANNING ARBORESCENCE. Given a directed weighted graph and a ROOT, an arborescence is a spanning tree where every non-root vertex has exactly one incoming edge and is reachable from the root -- the cheapest one-way broadcast tree. Greedy MST algorithms (Kruskal, Prim) fail because directions matter: picking each vertex's cheapest incoming edge can form a cycle a tree can't contain. The CHU-LIU/EDMONDS algorithm fixes exactly that. Pick the cheapest incoming edge everywhere; if the result is acyclic it is optimal. If a cycle forms, CONTRACT it to a super-vertex, reweighting each edge entering the cycle by subtracting the in-cycle edge it would replace, and recurse; expanding the contractions -- breaking each cycle at the one vertex entered from outside -- rebuilds the true minimum. The reweighting correctly credits that entering a cycle lets one of its internal edges be dropped. Verified against brute force -- enumerating every choice of one incoming edge per vertex, keeping the valid arborescences, and confirming the minimum weight -- on 500 random graphs, plus trees, unreachable vertices, multi-edges, and nested cycles; the returned edges always form a genuine spanning arborescence.
Minimum spanning arborescence: cheapest one-way broadcast tree
6 nodes, 9 directed edges, root = 0
greedy 'cheapest incoming edge per node' would pick:
node 1 <- node 3 (cost 4)
node 2 <- node 1 (cost 2)
node 3 <- node 2 (cost 3)
node 4 <- node 2 (cost 6)
node 5 <- node 3 (cost 5)
...but 1<-3, 2<-1, 3<-2 form a CYCLE (1->2->3->1), not a tree -- greedy fails.
Chu-Liu/Edmonds minimum arborescence (total cost 26):
node 1 <- node 0 (cost 10)
node 2 <- node 1 (cost 2)
node 3 <- node 2 (cost 3)
node 4 <- node 2 (cost 6)
node 5 <- node 3 (cost 5)
verified against brute force over all arborescences: True
The algorithm contracts the cheap cycle into a super-node, reweights edges entering it
by what they'd save, and recurses -- then expands, breaking each cycle at the one node
reached from outside. This is the directed analogue of the minimum spanning tree, where
Kruskal and Prim don't apply because edge directions constrain the tree.
wrote C:\Users\acwic\symplectic-nbody\examples\output\arborescence.svgSteiner tree: cheapest network through junctions
The minimum spanning tree connects ALL vertices; the STEINER TREE connects only a chosen subset -- the TERMINALS -- at least cost, free to route through other vertices (STEINER POINTS) when cheaper. It is the true shape of network design: the cheapest fibre backbone linking some cities (routing through junction towns allowed), a chip net touching a set of pins, a phylogenetic tree through inferred ancestors. It is NP-hard in general, but for a small number of terminals k the DREYFUS-WAGNER bitmask DP solves it exactly. It fills dp[S][v] = the minimum weight of a tree connecting terminal subset S and reaching vertex v, by two rules: MERGE two disjoint sub-trees for S1 and S2 rooted at the same v (dp[S][v] = min over splits of dp[S1][v]+dp[S2][v]), and GROW a tree toward a neighbour along a shortest path (a Dijkstra sweep per subset layer). The answer is min over v of dp[full][v], in O(3^k n + 2^k n^2) -- exponential only in the terminal count, not the graph size. Verified against brute force -- enumerating every subset of Steiner points and taking the induced MST -- on 400 random graphs, against the MST when every vertex is a terminal, and on cases where a Steiner point strictly beats the terminal-only tree.
Steiner tree: cheapest network connecting terminals via optional junctions
terminals: ['NW', 'NE', 'SE', 'SW']
a hub vertex 'hub' is available as a Steiner point (not required)
terminal-only MST (perimeter, no hub): 30
Steiner tree (allowed to use the hub): 12
saving from routing through the hub: 18
verified against brute force: True
Steiner cost for various terminal sets:
['NW', 'SE']: 6
['NW', 'NE', 'SE']: 9
['NW', 'NE', 'SE', 'SW']: 12
The Dreyfus-Wagner DP fills dp[S][v] = cheapest tree connecting terminal set S and
reaching vertex v, by MERGING disjoint terminal subsets at a shared root and GROWING
along shortest paths. Exponential only in the number of terminals -- so many terminals
in a large graph stay tractable, unlike brute-forcing every subset of Steiner points.
wrote C:\Users\acwic\symplectic-nbody\examples\output\steiner_tree.svgAHU tree isomorphism: same shape, in linear time
Two trees are ISOMORPHIC if one can be relabelled into the other -- same shape, different names. Deciding this for general GRAPHS is a famous open problem with no known polynomial algorithm, but for TREES the Aho-Hopcroft-Ullman algorithm does it in linear time via a CANONICAL FORM: a string identical for isomorphic trees and different otherwise. For a ROOTED tree it is built bottom-up -- each leaf is '()', each internal node SORTS its children's strings, concatenates, and wraps them in parentheses, so the encoding ignores the order children happen to be listed. For an UNROOTED tree there is no distinguished root, so AHU roots at the tree's CENTER -- the one or two vertices at the middle of its longest path, found by repeatedly peeling leaves -- which is isomorphism-invariant, giving a canonical form for the whole tree (comparing both forms when there are two centers). This powers deduplicating parse and syntax trees, matching acyclic molecules, and comparing phylogenies. Verified against brute force -- two trees are isomorphic iff some vertex permutation maps one edge set onto the other -- on hundreds of random trees, including relabelled copies (always isomorphic) and same-size different-shape trees (never), with canonical-form equality shown to coincide exactly with isomorphism.
AHU tree isomorphism: a canonical string that is equal iff trees match
tree A edges: [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5)]
rooted canonical form (root 0): ((()())(()))
-> each leaf is '()', each node sorts and wraps its children's forms
tree B = tree A with vertices shuffled: [(5, 2), (5, 4), (2, 0), (2, 1), (4, 3)]
isomorphic(A, B)? True (brute: True)
tree C = a 6-vertex path: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
isomorphic(A, C)? False (brute: False)
unrooted canonical forms (rooted at the center for canonicity):
A: ((()())(()))
B: ((()())(())) (equal to A -> isomorphic)
C: (((()))(()))|(((()))(())) (differs -> not isomorphic)
centers -- A: [0], C (path): [2, 3]
Rooting at the center makes the form canonical: every tree has 1 or 2 centers (the
middle of its longest path), found by peeling leaves. General GRAPH isomorphism has no
known polynomial algorithm, but trees fall in linear time to this bottom-up hashing.
wrote C:\Users\acwic\symplectic-nbody\examples\output\tree_isomorphism.svgEulerian trails: every edge once, from Konigsberg to Hierholzer
An EULERIAN CIRCUIT walks every edge of a graph exactly once and returns to its start; an EULERIAN PATH does so but may end elsewhere. The question is the founding problem of graph theory -- Euler's 1736 proof that the Seven Bridges of Konigsberg cannot all be crossed once -- and it drives genome assembly (an Eulerian path through a De Bruijn graph of k-mers), route planning that covers every street, and figure-drawing without lifting the pen. The existence conditions are complete: an UNDIRECTED connected graph has a circuit iff every vertex has even degree, a path iff exactly two vertices are odd (those being the endpoints); a DIRECTED graph has a circuit iff every vertex is balanced (in-degree = out-degree) and weakly connected, a path iff exactly one vertex has one extra out-edge (the start) and one an extra in-edge (the end). When a trail exists, HIERHOLZER'S ALGORITHM finds it in linear time: walk until stuck, forming a closed sub-tour, then splice in detours from any vertex with unused edges until every edge is consumed. This module classifies undirected and directed multigraphs and builds the trail iteratively. Verified: the existence predicate matches the degree/connectivity definition and the returned trail uses every edge exactly once with real adjacent steps, on hundreds of random graphs -- including Konigsberg (correctly impossible) and multigraphs.
Eulerian paths and circuits: every edge exactly once triangle Eulerian CIRCUIT (closed) trail: 0->1->2->0 path 0-1-2-3 Eulerian PATH (open) trail: 0->1->2->3 Konigsberg bridges no Eulerian trail K4 (complete, 4 nodes) no Eulerian trail bowtie (two triangles) Eulerian CIRCUIT (closed) trail: 0->1->2->0->3->4->0 The Seven Bridges of Konigsberg has all four land masses at odd degree, so no walk can cross every bridge exactly once -- Euler's 1736 proof, the birth of graph theory. Hierholzer builds a trail by walking until stuck (a closed sub-tour), then splicing in detours from any vertex with unused edges, until every edge is consumed -- linear time. bowtie trail valid + closed: True wrote C:\Users\acwic\symplectic-nbody\examples\output\eulerian.svg
Yen's algorithm: the K best alternative routes
Shortest-path algorithms find THE cheapest route; navigation apps, resilient networks, and itinerary planners need the K cheapest ALTERNATIVES. YEN'S ALGORITHM (1971) finds the K shortest LOOPLESS (simple, no repeated vertex) paths from source to target in a weighted directed graph, in increasing cost order -- the looplessness is what keeps the alternatives meaningful rather than the best path padded with pointless detours. It elaborates repeated shortest-path search: the first path is plain Dijkstra, and each next path is found by considering every SPUR NODE along the previous one, temporarily removing the edges that already-found paths took out of that node (forcing a different continuation) and the earlier root-path nodes (to stay loopless), then running Dijkstra from the spur to the target. The prefix plus this spur is a CANDIDATE; a priority queue keeps candidates in cost order and the cheapest unused one becomes the next path. Verified against brute force -- enumerating every simple source-to-target path, sorting by cost, and comparing the first K -- on hundreds of random graphs, confirming the paths are loopless, valid, distinct, non-decreasing in cost, and exactly the K cheapest.
Yen's algorithm: the K shortest loopless routes from A to F
network: 10 one-way roads among 6 junctions
4 shortest routes (increasing cost):
1. cost 13: A -> C -> B -> D -> E -> F
2. cost 14: A -> B -> D -> E -> F
3. cost 14: A -> C -> B -> D -> F
4. cost 15: A -> B -> D -> F
matches brute-force enumeration of all simple paths: True
Each next route is found by taking a prefix of the previous one, banning the edges
already used out of the spur node (to force a different continuation) and the earlier
nodes (to stay loopless), then running Dijkstra from the spur to the target. The cheapest
such candidate becomes the next route -- alternatives that are genuinely distinct, not
the best path padded with detours.
wrote C:\Users\acwic\symplectic-nbody\examples\output\yen_ksp.svgKarger's algorithm: minimum cut by random contraction
The GLOBAL MINIMUM CUT is the fewest edges (or least weight) whose removal splits a graph in two -- its weakest point. Deterministic algorithms (Stoer-Wagner) find it exactly; KARGER'S ALGORITHM (1993) takes a startlingly simple RANDOMIZED route built on one operation: CONTRACTION. Pick a random edge, merge its endpoints into a super-vertex (keeping parallel edges, dropping self-loops), and repeat until two super-vertices remain -- the edges between them are a cut, and with decent probability the minimum one. A given minimum cut survives a run only if none of its few edges is ever contracted, which happens with probability at least 2/(n(n-1)); repeating O(n^2 log n) times makes the failure chance vanish. The KARGER-STEIN refinement contracts only to ~n/sqrt(2) vertices -- where a min-cut edge is still unlikely hit -- then recurses twice and keeps the better, for O(n^2 log^3 n). Weighted graphs work identically by choosing edges with probability proportional to weight. Being Monte Carlo, correctness is checked STATISTICALLY: a contraction cut is always valid so never below the true minimum, and given enough trials it equals the exact Stoer-Wagner value -- confirmed on hundreds of random weighted graphs, with a fixed seed for reproducibility.
Karger's algorithm: minimum cut by random edge contraction
6 nodes in two heavy triangles joined by two light bridges (2-3 and 0-5)
exact minimum cut (Stoer-Wagner): 2, side [3, 4, 5]
best cut found as random trials accumulate:
after 1 trials: best = 2 <- found the true minimum
after 2 trials: best = 2 <- found the true minimum
after 5 trials: best = 2 <- found the true minimum
after 10 trials: best = 2 <- found the true minimum
after 25 trials: best = 2 <- found the true minimum
after 50 trials: best = 2 <- found the true minimum
after 100 trials: best = 2 <- found the true minimum
63/100 single trials happened to hit the minimum -- each is unlikely alone, but
repetition makes finding it almost certain.
repeated Karger: 2 (matches exact: True)
Karger-Stein: 2 (matches exact: True)
cut partition: [0, 1, 2] | [3, 4, 5]
Each trial merges random edges until two super-nodes remain; the edges between them are
a cut. A given minimum cut survives only if none of its few edges is ever contracted --
probability >= 2/(n(n-1)) per run -- so O(n^2 log n) runs drive the failure chance to ~0.
wrote C:\Users\acwic\symplectic-nbody\examples\output\karger.svgThree-body stability map
Every pixel is a full three-body integration; colour is the time until a body escapes. The fractal boundary between long-lived and quickly-ionized initial conditions is chaos drawn in IC space. (Generated separately by stability_map_demo.py -- it's compute-heavy.)
Chaos & the Lyapunov exponent
Two trajectories started 1e-9 apart diverge to order unity on an exponential clock. The pythagorean 3-body has a large positive Lyapunov exponent; a regular orbit's estimate decays toward zero.
Chaos in the three-body problem: the largest Lyapunov exponent
pythagorean 3-body : lambda = 0.523 Lyapunov time ~ 1.9 time units
regular two-body : lambda = 0.035 (decays toward 0 with T)
=> the chaotic system's exponent is 15x larger.
separation of two trajectories started 1e-9 apart (log scale):
..::::----------------===============+=======++++++++++++************************************#################@
grew from 1e-9 to ~1.3e-03 -- 6 orders of magnitude. That's why the long-term 3-body problem is unpredictable.
wrote C:\Users\acwic\symplectic-nbody\examples\output\chaos_divergence.svgMercury's perihelion precession (general relativity)
A first post-Newtonian correction makes the orbit slowly rotate instead of closing. The closed form gives Mercury's famous 43 arcsec/century; direct integration reproduces it.
Perihelion precession of Mercury (1PN general relativity)
analytic advance at the real speed of light: 42.98 arcsec/century
observed / GR-predicted value: ~43 arcsec/century
numeric integration reproduces the closed form 6*pi*GM/(c^2 a(1-e^2)):
c factor numeric/orbit analytic/orbit ratio
---------------------------------------------------
c/300 4.514734e-02 4.517161e-02 0.9995
c/400 8.019446e-02 8.030509e-02 0.9986
c/600 1.798247e-01 1.806865e-01 0.9952
wrote C:\Users\acwic\symplectic-nbody\examples\output\precession_rosette.svg
At the true speed of light the same rotation is a mere 43 arcsec/century --
undetectable in one orbit, unmistakable over a century of Mercury's.Lagrange points & zero-velocity curves
Five equilibria of the rotating-frame restricted 3-body problem. JWST parks at L2; the Trojan asteroids live at L4/L5.
Earth-Moon CR3BP (mu = 0.01215), nondimensional units point x y Jacobi C -------------------------------------------- L1 0.836918 0.000000 3.188336 L2 1.155680 0.000000 3.172156 L3 -1.005062 0.000000 3.012147 L4 0.487850 0.866025 2.987998 L5 0.487850 -0.866025 2.987998 wrote C:\Users\acwic\symplectic-nbody\examples\output\lagrange.svg L4/L5 (equilateral points) are stable for the Earth-Moon mass ratio; that's why Trojan asteroids cluster at the Sun-Jupiter L4/L5.
Hohmann transfer & mission delta-v
The cheapest two-burn hop between circular orbits sets every mission's delta-v budget: ~3.9 km/s from LEO to GEO, ~5.6 km/s and 259 days from Earth to Mars. The launch phase angle is why Mars windows open only every ~26 months.
Hohmann transfer: the cheapest way between two circular orbits
LEO (200 km) -> GEO (35786 km):
burn 1 = 2455 m/s, burn 2 = 1477 m/s, total = 3932 m/s
transfer time = 5.26 hours
propellant: m0/mf = 2.44 (LH2/LOX, ve=4.4 km/s)
Earth -> Mars (heliocentric):
total delta-v = 5.60 km/s
transfer time = 259 days
launch phase angle = 44.4 deg
These delta-v budgets set the propellant mass via the rocket equation,
and the phase angle sets the launch window -- why Mars missions leave
Earth only every ~26 months.
wrote C:\Users\acwic\symplectic-nbody\examples\output\hohmann.svgOberth effect: burn low and fast
A burn's energy gain is v dv + dv^2/2, so the same dv buys far more energy deep in the gravity well where the ship moves fastest. The same burn escapes from periapsis but leaves the ship bound at apoapsis -- why probes dive in before an escape burn and powered flybys work.
The Oberth effect: the same burn buys more energy at high speed
same 1500 m/s burn on a 300 km x 35786 km orbit:
at periapsis (v=10.2 km/s): v_inf = 4.05 km/s (escapes)
at apoapsis (v=1.6 km/s): v_inf = 0.00 km/s (still bound)
energy-gain advantage of the periapsis burn: 4.6x
This is why probes dive toward a planet before their escape burn, and
why a powered gravity assist (an Oberth maneuver at closest approach)
extracts far more than the same burn in deep space.
wrote C:\Users\acwic\symplectic-nbody\examples\output\oberth.svgEscape & cosmic velocities
The speed thresholds of spaceflight: orbital v1 = sqrt(GM/r), escape v2 = sqrt(2) v1 (11.2 km/s from Earth), and ~42 km/s to leave the Solar System. Push v2 to the speed of light and you recover the Schwarzschild radius -- 3 km for the Sun.
Escape and cosmic velocities body v_orbit (km/s) v_escape (km/s) ----------------------------------------------- Moon 1.68 2.38 Mars 3.55 5.03 Earth 7.91 11.19 Jupiter 42.57 60.20 Sun 436.82 617.75 white dwarf 5000.33 7071.53 escape / orbital = sqrt(2) always. leaving the Solar System from Earth's orbit: 42.1 km/s set v_escape = c and you get the Schwarzschild radius: 2954 m for the Sun. wrote C:\Users\acwic\symplectic-nbody\examples\output/cosmic_velocities.svg
Atmospheric escape: which worlds keep air
A planet keeps a gas only if its escape speed beats ~6x the molecules' thermal speed (Jeans parameter lambda >= 36). Earth loses H2 and He but keeps N2/O2; the Moon and Mars lose the light gases; Jupiter keeps even hydrogen -- exactly the atmospheres we observe.
Atmospheric escape: which worlds keep which gases (lambda >= 36 = keep) body H2 He H2O N2 CO2 -------------------------------------------------- Moon lose lose lose lose keep Mars lose lose keep keep keep Earth lose lose keep keep keep Jupiter keep keep keep keep keep Earth loses H2/He but keeps N2/O2/CO2; the Moon and Mars lose the light gases; Jupiter keeps everything. Retention needs the escape speed to beat ~6x the molecules' thermal speed. wrote C:\Users\acwic\symplectic-nbody\examples\output\atmosphere.svg