lfm-physics 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/CHANGELOG.md +17 -0
  2. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/PKG-INFO +51 -1
  3. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/README.md +50 -0
  4. lfm_physics-0.3.0/docs/primer.md +117 -0
  5. lfm_physics-0.3.0/docs/troubleshooting.md +159 -0
  6. lfm_physics-0.3.0/examples/15_visualization.py +93 -0
  7. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/__init__.py +11 -1
  8. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/__init__.py +7 -0
  9. lfm_physics-0.3.0/lfm/analysis/spectrum.py +57 -0
  10. lfm_physics-0.3.0/lfm/analysis/tracker.py +80 -0
  11. lfm_physics-0.3.0/lfm/sweep.py +80 -0
  12. lfm_physics-0.3.0/lfm/viz/__init__.py +42 -0
  13. lfm_physics-0.3.0/lfm/viz/_util.py +14 -0
  14. lfm_physics-0.3.0/lfm/viz/evolution.py +118 -0
  15. lfm_physics-0.3.0/lfm/viz/fields.py +78 -0
  16. lfm_physics-0.3.0/lfm/viz/radial.py +87 -0
  17. lfm_physics-0.3.0/lfm/viz/slices.py +150 -0
  18. lfm_physics-0.3.0/lfm/viz/spectrum.py +71 -0
  19. lfm_physics-0.3.0/lfm/viz/sweep.py +60 -0
  20. lfm_physics-0.3.0/lfm/viz/tracker.py +75 -0
  21. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/pyproject.toml +1 -1
  22. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.github/workflows/publish.yml +0 -0
  23. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.github/workflows/test.yml +0 -0
  24. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.gitignore +0 -0
  25. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.readthedocs.yaml +0 -0
  26. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/CONTRIBUTING.md +0 -0
  27. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/LICENSE +0 -0
  28. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/benchmarks/README.md +0 -0
  29. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/benchmarks/bench_evolver.py +0 -0
  30. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/benchmarks/bench_fields.py +0 -0
  31. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/analysis.rst +0 -0
  32. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/config.rst +0 -0
  33. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/constants.rst +0 -0
  34. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/core.rst +0 -0
  35. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/fields.rst +0 -0
  36. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/io.rst +0 -0
  37. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/simulation.rst +0 -0
  38. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/units.rst +0 -0
  39. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/changelog.rst +0 -0
  40. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/conf.py +0 -0
  41. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/contributing.rst +0 -0
  42. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/examples.md +0 -0
  43. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/index.rst +0 -0
  44. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/installation.md +0 -0
  45. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/quickstart.md +0 -0
  46. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/requirements.txt +0 -0
  47. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/01_empty_space.py +0 -0
  48. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/02_first_particle.py +0 -0
  49. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/03_measuring_gravity.py +0 -0
  50. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/04_two_bodies.py +0 -0
  51. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/05_electric_charge.py +0 -0
  52. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/06_dark_matter.py +0 -0
  53. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/07_matter_creation.py +0 -0
  54. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/08_universe.py +0 -0
  55. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/09_hydrogen_atom.py +0 -0
  56. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/10_hydrogen_molecule.py +0 -0
  57. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/11_oxygen.py +0 -0
  58. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/12_fluid_dynamics.py +0 -0
  59. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/13_weak_force.py +0 -0
  60. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/14_strong_force.py +0 -0
  61. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/color.py +0 -0
  62. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/energy.py +0 -0
  63. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/metrics.py +0 -0
  64. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/observables.py +0 -0
  65. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/structure.py +0 -0
  66. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/config.py +0 -0
  67. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/constants.py +0 -0
  68. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/__init__.py +0 -0
  69. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/__init__.py +0 -0
  70. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/cupy_backend.py +0 -0
  71. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/kernel_source.py +0 -0
  72. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/numpy_backend.py +0 -0
  73. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/protocol.py +0 -0
  74. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/evolver.py +0 -0
  75. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/integrator.py +0 -0
  76. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/stencils.py +0 -0
  77. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/__init__.py +0 -0
  78. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/arrangements.py +0 -0
  79. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/equilibrium.py +0 -0
  80. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/random.py +0 -0
  81. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/soliton.py +0 -0
  82. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/io/__init__.py +0 -0
  83. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/py.typed +0 -0
  84. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/simulation.py +0 -0
  85. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/units.py +0 -0
  86. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/paper_experiments/why_is_c_what_it_is.py +0 -0
  87. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/__init__.py +0 -0
  88. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/conftest.py +0 -0
  89. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_analysis.py +0 -0
  90. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_backends.py +0 -0
  91. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_config.py +0 -0
  92. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_constants.py +0 -0
  93. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_evolver.py +0 -0
  94. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_fields.py +0 -0
  95. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_integrator.py +0 -0
  96. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_simulation.py +0 -0
  97. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_stencils.py +0 -0
  98. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_03_3d_lattice.png +0 -0
  99. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_07_3d_lattice.png +0 -0
  100. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_08_3d_lattice.png +0 -0
  101. {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_12_3d_lattice.png +0 -0
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.3.0] - 2026-03-21
8
+
9
+ ### Added
10
+ - **Visualisation module** (`lfm.viz`): 10 plotting functions for rapid exploration
11
+ - `plot_slice`, `plot_three_slices`, `plot_chi_histogram` — 2D field slices
12
+ - `plot_evolution`, `plot_energy_components` — time-series dashboards
13
+ - `plot_radial_profile` — χ(r) with 1/r reference overlay
14
+ - `plot_isosurface` — 3D voxel rendering of χ wells/voids
15
+ - `plot_power_spectrum` — Fourier P(k) visualisation
16
+ - `plot_trajectories` — peak motion scatter plots
17
+ - `plot_sweep` — parameter sweep line plots
18
+ - **Power spectrum analyser** (`lfm.analysis.spectrum`): `power_spectrum()` — radially-averaged FFT P(k) for any 3D field
19
+ - **Particle tracker** (`lfm.analysis.tracker`): `track_peaks()` and `flatten_trajectories()` — follow energy-density maxima across timesteps
20
+ - **Parameter sweep runner** (`lfm.sweep`): `sweep()` — run a batch of simulations varying one parameter, collect metrics
21
+ - **Docs**: `docs/troubleshooting.md` — common errors (NaN, CFL, slow, imports) with fixes
22
+ - **Docs**: `docs/primer.md` — "LFM in Five Minutes" physics primer
23
+
7
24
  ## [0.2.1] - 2026-03-20
8
25
 
9
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfm-physics
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Lattice Field Medium physics simulation library
5
5
  Project-URL: Homepage, https://github.com/gpartin/lfm-physics
6
6
  Project-URL: Repository, https://github.com/gpartin/lfm-physics
@@ -214,6 +214,56 @@ fc = lfm.color_variance(psi_real_color, psi_imag_color)
214
214
 
215
215
  # Confinement proxy — χ line integral between peaks
216
216
  proxy = lfm.confinement_proxy(sim.chi, pos_a, pos_b)
217
+
218
+ # Fourier power spectrum P(k) of any 3D field
219
+ spec = lfm.power_spectrum(sim.chi, bins=50)
220
+ # spec['k'], spec['power']
221
+
222
+ # Track energy peaks across a run
223
+ trajectories = lfm.track_peaks(sim, steps=5000, interval=200, n_peaks=3)
224
+ ```
225
+
226
+ ## Parameter Sweeps
227
+
228
+ ```python
229
+ # Sweep amplitude from 2 to 10 and record chi_min at each value
230
+ config = lfm.SimulationConfig(grid_size=32)
231
+ results = lfm.sweep(config, param="amplitude", values=[2, 4, 6, 8, 10],
232
+ steps=3000, metric_names=["chi_min", "well_fraction"])
233
+ for r in results:
234
+ print(f"amp={r['amplitude']:.0f} chi_min={r['chi_min']:.2f}")
235
+ ```
236
+
237
+ ## Visualisation *(New in 0.3.0)*
238
+
239
+ Install with: `pip install "lfm-physics[viz]"`
240
+
241
+ ```python
242
+ from lfm.viz import (
243
+ plot_slice, # 2D slice through a 3D field
244
+ plot_three_slices, # XY + XZ + YZ panels
245
+ plot_chi_histogram, # distribution of χ values
246
+ plot_evolution, # time-series of metrics
247
+ plot_energy_components, # stacked kinetic / gradient / potential
248
+ plot_radial_profile, # χ(r) with 1/r reference overlay
249
+ plot_isosurface, # 3D voxel rendering
250
+ plot_power_spectrum, # P(k) from Fourier analysis
251
+ plot_trajectories, # peak motion in x-y / x-z / y-z
252
+ plot_sweep, # sweep results line plot
253
+ )
254
+
255
+ # Example: slice through the chi field at z = 32
256
+ fig, ax = plot_slice(sim.chi, axis=2, index=32, title="χ mid-plane")
257
+ fig.savefig("chi_slice.png")
258
+
259
+ # Three-panel overview
260
+ fig = plot_three_slices(sim.chi, title="χ field")
261
+
262
+ # Time evolution dashboard
263
+ fig = plot_evolution(sim.history)
264
+
265
+ # Radial profile with 1/r fit
266
+ fig, ax = plot_radial_profile(sim.chi, center=(32,32,32))
217
267
  ```
218
268
 
219
269
  ## Checkpoints & Units
@@ -172,6 +172,56 @@ fc = lfm.color_variance(psi_real_color, psi_imag_color)
172
172
 
173
173
  # Confinement proxy — χ line integral between peaks
174
174
  proxy = lfm.confinement_proxy(sim.chi, pos_a, pos_b)
175
+
176
+ # Fourier power spectrum P(k) of any 3D field
177
+ spec = lfm.power_spectrum(sim.chi, bins=50)
178
+ # spec['k'], spec['power']
179
+
180
+ # Track energy peaks across a run
181
+ trajectories = lfm.track_peaks(sim, steps=5000, interval=200, n_peaks=3)
182
+ ```
183
+
184
+ ## Parameter Sweeps
185
+
186
+ ```python
187
+ # Sweep amplitude from 2 to 10 and record chi_min at each value
188
+ config = lfm.SimulationConfig(grid_size=32)
189
+ results = lfm.sweep(config, param="amplitude", values=[2, 4, 6, 8, 10],
190
+ steps=3000, metric_names=["chi_min", "well_fraction"])
191
+ for r in results:
192
+ print(f"amp={r['amplitude']:.0f} chi_min={r['chi_min']:.2f}")
193
+ ```
194
+
195
+ ## Visualisation *(New in 0.3.0)*
196
+
197
+ Install with: `pip install "lfm-physics[viz]"`
198
+
199
+ ```python
200
+ from lfm.viz import (
201
+ plot_slice, # 2D slice through a 3D field
202
+ plot_three_slices, # XY + XZ + YZ panels
203
+ plot_chi_histogram, # distribution of χ values
204
+ plot_evolution, # time-series of metrics
205
+ plot_energy_components, # stacked kinetic / gradient / potential
206
+ plot_radial_profile, # χ(r) with 1/r reference overlay
207
+ plot_isosurface, # 3D voxel rendering
208
+ plot_power_spectrum, # P(k) from Fourier analysis
209
+ plot_trajectories, # peak motion in x-y / x-z / y-z
210
+ plot_sweep, # sweep results line plot
211
+ )
212
+
213
+ # Example: slice through the chi field at z = 32
214
+ fig, ax = plot_slice(sim.chi, axis=2, index=32, title="χ mid-plane")
215
+ fig.savefig("chi_slice.png")
216
+
217
+ # Three-panel overview
218
+ fig = plot_three_slices(sim.chi, title="χ field")
219
+
220
+ # Time evolution dashboard
221
+ fig = plot_evolution(sim.history)
222
+
223
+ # Radial profile with 1/r fit
224
+ fig, ax = plot_radial_profile(sim.chi, center=(32,32,32))
175
225
  ```
176
226
 
177
227
  ## Checkpoints & Units
@@ -0,0 +1,117 @@
1
+ # LFM in Five Minutes
2
+
3
+ A minimal introduction to the physics behind the `lfm-physics` library.
4
+
5
+ ---
6
+
7
+ ## One Sentence
8
+
9
+ > The universe is a cubic lattice where every point stores two numbers —
10
+ > a wave amplitude **Ψ** and a local stiffness **χ** — and they evolve
11
+ > by two coupled wave equations.
12
+
13
+ ---
14
+
15
+ ## The Two Fields
16
+
17
+ | Symbol | Name | Physical meaning |
18
+ |--------|------|------------------|
19
+ | **Ψ** | Wave field | Energy / matter at each lattice point |
20
+ | **χ** | Chi field | Stiffness of the lattice at each point |
21
+
22
+ Empty space has χ = 19 everywhere. Where energy concentrates, χ drops
23
+ below 19, forming a potential well — what we call *gravity*.
24
+
25
+ ---
26
+
27
+ ## The Two Equations
28
+
29
+ **GOV-01 — Wave Equation** (how Ψ evolves):
30
+
31
+ ```
32
+ Ψⁿ⁺¹ = 2Ψⁿ − Ψⁿ⁻¹ + Δt²[c²∇²Ψⁿ − (χⁿ)²Ψⁿ]
33
+ ```
34
+
35
+ Energy propagates through the lattice. The `χ²Ψ` term means waves
36
+ oscillate faster where χ is high (empty space) and slower where χ is low
37
+ (near matter). This is how gravity bends light.
38
+
39
+ **GOV-02 — Chi Equation** (how χ evolves):
40
+
41
+ ```
42
+ χⁿ⁺¹ = 2χⁿ − χⁿ⁻¹ + Δt²[c²∇²χⁿ − κ(|Ψⁿ|² − E₀²)]
43
+ ```
44
+
45
+ Energy density |Ψ|² pushes χ down — matter curves spacetime. The
46
+ coupling κ = 1/63 is derived from the lattice geometry.
47
+
48
+ **That's it.** These two update rules, applied at every lattice point
49
+ every timestep, produce gravity, waves, dark matter, expansion, and more.
50
+
51
+ ---
52
+
53
+ ## Why 19?
54
+
55
+ The number 19 comes from counting non-propagating modes on a 3D cubic
56
+ lattice:
57
+
58
+ | Mode type | Count | k-vectors |
59
+ |-----------|-------|-----------|
60
+ | Centre | 1 | (0,0,0) |
61
+ | Faces | 6 | (±1,0,0), (0,±1,0), (0,0,±1) |
62
+ | Edges | 12 | (±1,±1,0), etc. |
63
+ | **Total** | **19** | **= χ₀** |
64
+
65
+ The remaining 8 corner modes (±1,±1,±1) are propagating — identifying
66
+ them with gluons gives N_gluons = 8.
67
+
68
+ From χ₀ = 19 alone, LFM derives 40+ physical constants to high accuracy.
69
+
70
+ ---
71
+
72
+ ## What Emerges
73
+
74
+ | Phenomenon | How it arises |
75
+ |------------|---------------|
76
+ | **Gravity** | Energy (|Ψ|²) dips χ → potential well → attraction |
77
+ | **Dark matter** | χ wells persist after matter moves away (memory) |
78
+ | **Electromagnetism** | Phase of complex Ψ = charge; interference = Coulomb |
79
+ | **Expansion** | Voids evacuate → χ rises → photons slow down |
80
+ | **Particles** | Standing waves trapped in self-consistent χ wells |
81
+ | **Atoms** | Nuclear χ well + bound electron eigenmodes |
82
+
83
+ ---
84
+
85
+ ## Quick Start
86
+
87
+ ```python
88
+ import lfm
89
+
90
+ # Create a 64-cell cubic universe
91
+ config = lfm.SimulationConfig(grid_size=64)
92
+ sim = lfm.Simulation(config)
93
+
94
+ # Drop a soliton — a localized energy concentration
95
+ sim.place_soliton((32, 32, 32), amplitude=6.0)
96
+
97
+ # Evolve for 1000 timesteps
98
+ sim.run(steps=1000)
99
+
100
+ # Look at what happened
101
+ print(f"χ_min = {sim.chi.min():.2f}") # Should be < 19 (gravity!)
102
+ ```
103
+
104
+ See the [examples/](../examples/) directory for 14 runnable demos covering
105
+ gravity, electromagnetism, atoms, orbits, cosmology, and more.
106
+
107
+ ---
108
+
109
+ ## Going Deeper
110
+
111
+ | Topic | Resource |
112
+ |-------|----------|
113
+ | Full API reference | `help(lfm.Simulation)` |
114
+ | All 14 examples | [examples/](../examples/) |
115
+ | Visualization | `from lfm.viz import plot_slice` |
116
+ | Parameter sweeps | `from lfm import sweep` |
117
+ | Common errors | [troubleshooting.md](troubleshooting.md) |
@@ -0,0 +1,159 @@
1
+ # Troubleshooting
2
+
3
+ Common problems and how to fix them.
4
+
5
+ ---
6
+
7
+ ## My simulation gives NaN
8
+
9
+ **Cause:** The wave amplitude is too large for the grid, causing the
10
+ leapfrog integrator to diverge.
11
+
12
+ **Fix:**
13
+ ```python
14
+ # Lower the amplitude
15
+ sim.place_soliton((32, 32, 32), amplitude=4.0) # instead of 12.0
16
+
17
+ # Or use a larger grid (more room to spread energy)
18
+ config = lfm.SimulationConfig(grid_size=128) # instead of 32
19
+ ```
20
+
21
+ **Rule of thumb:** Keep amplitude below ~8 for `grid_size=32`, below ~12
22
+ for `grid_size=64`. When in doubt, start small and increase.
23
+
24
+ ---
25
+
26
+ ## Energy diverges over time
27
+
28
+ **Cause:** The timestep `dt` exceeds the CFL stability limit.
29
+
30
+ **Fix:** Use the default `dt=0.02` — it satisfies the CFL condition for
31
+ all standard setups. If you've changed `dt` manually:
32
+
33
+ ```python
34
+ # Safe: dt = 0.02 (default, well inside CFL)
35
+ config = lfm.SimulationConfig(dt=0.02)
36
+
37
+ # The CFL limit for the 19-point stencil with χ₀=19:
38
+ # dt < 1/sqrt(16c²/(3·Δx²) + χ₀²) ≈ 0.104 (for Δx=c=1)
39
+ # Our default 0.02 gives ~5× safety margin.
40
+ ```
41
+
42
+ ---
43
+
44
+ ## χ goes negative
45
+
46
+ **This is not a bug.** In extreme-gravity scenarios (very high amplitude
47
+ or dense clusters), χ can dip below zero. This is the LFM equivalent of
48
+ a black hole interior.
49
+
50
+ **If you want to prevent it:** Enable the Mexican-hat self-interaction,
51
+ which creates a stable second vacuum at −χ₀:
52
+
53
+ ```python
54
+ config = lfm.SimulationConfig(
55
+ lambda_self=lfm.LAMBDA_H, # 4/31 ≈ 0.129
56
+ )
57
+ ```
58
+
59
+ **If you don't care about extreme gravity:** Use gravity-only mode
60
+ (default `lambda_self=0.0`) and lower the amplitude.
61
+
62
+ ---
63
+
64
+ ## Everything collapses into one blob
65
+
66
+ **Cause:** With periodic boundaries and enough energy, gravity always
67
+ wins — everything falls toward the centre of mass. This is correct
68
+ physics for a closed universe.
69
+
70
+ **Fix:** Use frozen boundaries (the default) to simulate an open region
71
+ embedded in the vacuum:
72
+
73
+ ```python
74
+ config = lfm.SimulationConfig(
75
+ boundary_type=lfm.BoundaryType.FROZEN, # default
76
+ )
77
+ ```
78
+
79
+ Frozen boundaries hold χ = 19 at the edges, representing infinite empty
80
+ space beyond the simulation box.
81
+
82
+ ---
83
+
84
+ ## Simulation is very slow
85
+
86
+ **Check 1 — Are you using GPU?**
87
+ ```python
88
+ print(lfm.gpu_available()) # True if CuPy + CUDA detected
89
+
90
+ # Force GPU
91
+ sim = lfm.Simulation(config, backend="gpu")
92
+ ```
93
+
94
+ Install GPU support:
95
+ ```bash
96
+ pip install "lfm-physics[gpu]"
97
+ ```
98
+
99
+ **Check 2 — Is your grid too large for CPU?**
100
+
101
+ | Grid size | Cells | CPU time/step | GPU time/step |
102
+ |-----------|-----------|---------------|---------------|
103
+ | 32³ | 32 K | ~0.3 ms | ~0.02 ms |
104
+ | 64³ | 262 K | ~3 ms | ~0.05 ms |
105
+ | 128³ | 2.1 M | ~30 ms | ~0.3 ms |
106
+ | 256³ | 16.8 M | ~300 ms | ~7 ms |
107
+
108
+ For grid_size ≥ 128 on CPU, expect minutes for long runs. GPU gives
109
+ 50–200× speedup.
110
+
111
+ ---
112
+
113
+ ## ImportError: matplotlib not found
114
+
115
+ The visualization module (`lfm.viz`) requires matplotlib:
116
+
117
+ ```bash
118
+ pip install "lfm-physics[viz]"
119
+ ```
120
+
121
+ Or install everything:
122
+ ```bash
123
+ pip install "lfm-physics[all]"
124
+ ```
125
+
126
+ ---
127
+
128
+ ## How do I save and resume a run?
129
+
130
+ ```python
131
+ # Save
132
+ sim.save_checkpoint("my_run.npz")
133
+
134
+ # Resume later
135
+ sim2 = lfm.Simulation.load_checkpoint("my_run.npz")
136
+ sim2.run(steps=5000) # continues where it left off
137
+ ```
138
+
139
+ Checkpoints preserve the full simulation state: fields, config, step
140
+ count, and metric history.
141
+
142
+ ---
143
+
144
+ ## Which field level should I use?
145
+
146
+ | I want to simulate... | Field level |
147
+ |-----------------------------------|-----------------------|
148
+ | Gravity, dark matter, cosmology | `FieldLevel.REAL` |
149
+ | Electromagnetism, charged particles | `FieldLevel.COMPLEX` |
150
+ | Strong force, color confinement | `FieldLevel.COLOR` |
151
+
152
+ ```python
153
+ config = lfm.SimulationConfig(
154
+ field_level=lfm.FieldLevel.COMPLEX, # for EM
155
+ )
156
+ ```
157
+
158
+ Start with REAL (simplest, fastest). Upgrade only when your physics
159
+ requires it.
@@ -0,0 +1,93 @@
1
+ """15 – Visualisation & Analysis
2
+
3
+ The lfm.viz toolkit: plot slices, radial profiles, time-series
4
+ dashboards, power spectra, and parameter sweeps without writing
5
+ any matplotlib boilerplate.
6
+
7
+ Requires: pip install "lfm-physics[viz]"
8
+ """
9
+
10
+ import lfm
11
+
12
+ # ── 1. Run a quick simulation ─────────────────────────────────────────
13
+
14
+ config = lfm.SimulationConfig(grid_size=32)
15
+ sim = lfm.Simulation(config)
16
+ sim.place_soliton((16, 16, 16), amplitude=6.0)
17
+ sim.equilibrate()
18
+ sim.run(steps=3000)
19
+
20
+ print("15 – Visualisation & Analysis")
21
+ print("=" * 55)
22
+ print()
23
+
24
+ m = sim.metrics()
25
+ print(f" chi_min = {m['chi_min']:.2f} (gravity well depth)")
26
+ print(f" wells = {m['well_fraction']*100:.1f}%")
27
+ print()
28
+
29
+ # ── 2. 2D slice through the chi field ─────────────────────────────────
30
+
31
+ from lfm.viz import plot_slice, plot_three_slices
32
+
33
+ fig, ax = plot_slice(sim.chi, axis=2, index=16, title="χ mid-plane (z=16)")
34
+ fig.savefig("tutorial_15_slice.png", dpi=120, bbox_inches="tight")
35
+ print("Saved: tutorial_15_slice.png")
36
+
37
+ # Three-panel overview (XY, XZ, YZ)
38
+ fig = plot_three_slices(sim.chi, title="χ field — three planes")
39
+ fig.savefig("tutorial_15_three_slices.png", dpi=120, bbox_inches="tight")
40
+ print("Saved: tutorial_15_three_slices.png")
41
+
42
+ # ── 3. Radial profile with 1/r reference ──────────────────────────────
43
+
44
+ from lfm.viz import plot_radial_profile
45
+
46
+ fig, ax = plot_radial_profile(sim.chi, center=(16, 16, 16), max_radius=12)
47
+ fig.savefig("tutorial_15_radial.png", dpi=120, bbox_inches="tight")
48
+ print("Saved: tutorial_15_radial.png")
49
+
50
+ # ── 4. Chi histogram ──────────────────────────────────────────────────
51
+
52
+ from lfm.viz import plot_chi_histogram
53
+
54
+ fig, ax = plot_chi_histogram(sim.chi, title="χ distribution after 3000 steps")
55
+ fig.savefig("tutorial_15_histogram.png", dpi=120, bbox_inches="tight")
56
+ print("Saved: tutorial_15_histogram.png")
57
+
58
+ # ── 5. Time-evolution dashboard ───────────────────────────────────────
59
+
60
+ from lfm.viz import plot_evolution
61
+
62
+ fig = plot_evolution(sim.history, title="Metric evolution")
63
+ fig.savefig("tutorial_15_evolution.png", dpi=120, bbox_inches="tight")
64
+ print("Saved: tutorial_15_evolution.png")
65
+
66
+ # ── 6. Fourier power spectrum ─────────────────────────────────────────
67
+
68
+ from lfm.viz import plot_power_spectrum
69
+
70
+ fig, ax = plot_power_spectrum(sim.chi, title="P(k) of χ field")
71
+ fig.savefig("tutorial_15_spectrum.png", dpi=120, bbox_inches="tight")
72
+ print("Saved: tutorial_15_spectrum.png")
73
+
74
+ # ── 7. Parameter sweep ───────────────────────────────────────────────
75
+
76
+ from lfm.viz import plot_sweep
77
+
78
+ sweep_cfg = lfm.SimulationConfig(grid_size=32)
79
+ results = lfm.sweep(
80
+ sweep_cfg,
81
+ param="amplitude",
82
+ values=[2, 4, 6, 8],
83
+ steps=2000,
84
+ metric_names=["chi_min", "well_fraction"],
85
+ )
86
+ fig, ax = plot_sweep(results, x_param="amplitude", y_metric="chi_min",
87
+ title="χ_min vs soliton amplitude")
88
+ fig.savefig("tutorial_15_sweep.png", dpi=120, bbox_inches="tight")
89
+ print("Saved: tutorial_15_sweep.png")
90
+
91
+ print()
92
+ print("All plots saved. Open the PNG files to explore your simulation.")
93
+ print("Every lfm.viz function returns (fig, ax) so you can customise further.")
@@ -15,7 +15,7 @@ Quick start::
15
15
  print(sim.metrics())
16
16
  """
17
17
 
18
- __version__ = "0.2.2"
18
+ __version__ = "0.3.0"
19
19
 
20
20
  from lfm.analysis import (
21
21
  chi_statistics,
@@ -28,13 +28,16 @@ from lfm.analysis import (
28
28
  energy_conservation_drift,
29
29
  find_peaks,
30
30
  fit_power_law,
31
+ flatten_trajectories,
31
32
  fluid_fields,
32
33
  interior_mask,
33
34
  measure_force,
34
35
  measure_separation,
35
36
  momentum_density,
37
+ power_spectrum,
36
38
  radial_profile,
37
39
  total_energy,
40
+ track_peaks,
38
41
  void_fraction,
39
42
  weak_parity_asymmetry,
40
43
  well_fraction,
@@ -81,6 +84,7 @@ from lfm.fields import (
81
84
  wave_kick,
82
85
  )
83
86
  from lfm.simulation import Simulation
87
+ from lfm.sweep import sweep
84
88
  from lfm.units import CosmicScale, PlanckScale
85
89
 
86
90
  __all__ = [
@@ -154,6 +158,12 @@ __all__ = [
154
158
  "continuity_residual",
155
159
  # Color / Confinement
156
160
  "color_variance",
161
+ # Spectrum & Tracker
162
+ "power_spectrum",
163
+ "track_peaks",
164
+ "flatten_trajectories",
165
+ # Sweep
166
+ "sweep",
157
167
  # Units
158
168
  "CosmicScale",
159
169
  "PlanckScale",
@@ -21,6 +21,7 @@ from lfm.analysis.observables import (
21
21
  radial_profile,
22
22
  weak_parity_asymmetry,
23
23
  )
24
+ from lfm.analysis.spectrum import power_spectrum
24
25
  from lfm.analysis.structure import (
25
26
  chi_statistics,
26
27
  count_clusters,
@@ -28,6 +29,7 @@ from lfm.analysis.structure import (
28
29
  void_fraction,
29
30
  well_fraction,
30
31
  )
32
+ from lfm.analysis.tracker import flatten_trajectories, track_peaks
31
33
 
32
34
  __all__ = [
33
35
  # energy
@@ -55,4 +57,9 @@ __all__ = [
55
57
  "confinement_proxy",
56
58
  # color
57
59
  "color_variance",
60
+ # spectrum
61
+ "power_spectrum",
62
+ # tracker
63
+ "track_peaks",
64
+ "flatten_trajectories",
58
65
  ]
@@ -0,0 +1,57 @@
1
+ """Fourier power-spectrum analysis of lattice fields."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+
9
+ def power_spectrum(
10
+ field: NDArray,
11
+ bins: int = 50,
12
+ ) -> dict[str, NDArray]:
13
+ """Compute the radially-averaged power spectrum of a 3-D field.
14
+
15
+ Parameters
16
+ ----------
17
+ field : ndarray (N, N, N)
18
+ Scalar field (e.g. ``sim.chi`` or ``sim.energy_density``).
19
+ bins : int
20
+ Number of radial k-bins.
21
+
22
+ Returns
23
+ -------
24
+ dict
25
+ ``k`` — bin centres, ``power`` — P(k) in each bin,
26
+ ``counts`` — number of modes per bin.
27
+ """
28
+ if field.ndim != 3:
29
+ raise ValueError(f"Expected 3-D array, got shape {field.shape}")
30
+
31
+ N = field.shape[0]
32
+ fft = np.fft.fftn(field)
33
+ pk = np.abs(fft) ** 2 / field.size
34
+
35
+ kx = np.fft.fftfreq(N) * N
36
+ ky = np.fft.fftfreq(N) * N
37
+ kz = np.fft.fftfreq(N) * N
38
+ KX, KY, KZ = np.meshgrid(kx, ky, kz, indexing="ij")
39
+ K = np.sqrt(KX**2 + KY**2 + KZ**2)
40
+
41
+ k_max = N // 2
42
+ bin_edges = np.linspace(0.5, k_max + 0.5, bins + 1)
43
+ k_centres = 0.5 * (bin_edges[:-1] + bin_edges[1:])
44
+ power = np.zeros(bins)
45
+ counts = np.zeros(bins, dtype=int)
46
+
47
+ k_flat = K.ravel()
48
+ pk_flat = pk.ravel()
49
+ idx = np.digitize(k_flat, bin_edges) - 1
50
+
51
+ for i in range(bins):
52
+ mask = idx == i
53
+ counts[i] = mask.sum()
54
+ if counts[i] > 0:
55
+ power[i] = pk_flat[mask].mean()
56
+
57
+ return {"k": k_centres, "power": power, "counts": counts}