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.
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/CHANGELOG.md +17 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/PKG-INFO +51 -1
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/README.md +50 -0
- lfm_physics-0.3.0/docs/primer.md +117 -0
- lfm_physics-0.3.0/docs/troubleshooting.md +159 -0
- lfm_physics-0.3.0/examples/15_visualization.py +93 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/__init__.py +11 -1
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/__init__.py +7 -0
- lfm_physics-0.3.0/lfm/analysis/spectrum.py +57 -0
- lfm_physics-0.3.0/lfm/analysis/tracker.py +80 -0
- lfm_physics-0.3.0/lfm/sweep.py +80 -0
- lfm_physics-0.3.0/lfm/viz/__init__.py +42 -0
- lfm_physics-0.3.0/lfm/viz/_util.py +14 -0
- lfm_physics-0.3.0/lfm/viz/evolution.py +118 -0
- lfm_physics-0.3.0/lfm/viz/fields.py +78 -0
- lfm_physics-0.3.0/lfm/viz/radial.py +87 -0
- lfm_physics-0.3.0/lfm/viz/slices.py +150 -0
- lfm_physics-0.3.0/lfm/viz/spectrum.py +71 -0
- lfm_physics-0.3.0/lfm/viz/sweep.py +60 -0
- lfm_physics-0.3.0/lfm/viz/tracker.py +75 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/pyproject.toml +1 -1
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.github/workflows/publish.yml +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.github/workflows/test.yml +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.gitignore +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/.readthedocs.yaml +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/CONTRIBUTING.md +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/LICENSE +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/benchmarks/README.md +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/benchmarks/bench_evolver.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/benchmarks/bench_fields.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/analysis.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/config.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/constants.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/core.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/fields.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/io.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/simulation.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/api/units.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/changelog.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/conf.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/contributing.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/examples.md +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/index.rst +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/installation.md +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/quickstart.md +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/docs/requirements.txt +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/01_empty_space.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/02_first_particle.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/03_measuring_gravity.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/04_two_bodies.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/05_electric_charge.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/06_dark_matter.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/07_matter_creation.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/08_universe.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/09_hydrogen_atom.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/10_hydrogen_molecule.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/11_oxygen.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/12_fluid_dynamics.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/13_weak_force.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/examples/14_strong_force.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/color.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/energy.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/metrics.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/observables.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/analysis/structure.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/config.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/constants.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/__init__.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/__init__.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/cupy_backend.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/kernel_source.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/numpy_backend.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/backends/protocol.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/evolver.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/integrator.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/core/stencils.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/__init__.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/arrangements.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/equilibrium.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/random.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/fields/soliton.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/io/__init__.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/py.typed +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/simulation.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/lfm/units.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/paper_experiments/why_is_c_what_it_is.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/__init__.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/conftest.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_analysis.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_backends.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_config.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_constants.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_evolver.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_fields.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_integrator.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_simulation.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tests/test_stencils.py +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_03_3d_lattice.png +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_07_3d_lattice.png +0 -0
- {lfm_physics-0.2.2 → lfm_physics-0.3.0}/tutorial_08_3d_lattice.png +0 -0
- {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.
|
|
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.
|
|
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}
|