lfm-physics 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ LFM — Lattice Field Medium Physics Library
3
+ ===========================================
4
+
5
+ Two governing equations. One integer (χ₀ = 19). All of physics.
6
+
7
+ Quick start::
8
+
9
+ import lfm
10
+
11
+ print(lfm.CHI0) # 19.0
12
+ print(lfm.KAPPA) # 0.015873...
13
+ print(lfm.ALPHA_EM) # 0.007299... ≈ 1/137.088
14
+
15
+ config = lfm.SimulationConfig(grid_size=64)
16
+ sim = lfm.Simulation(config)
17
+ """
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ from lfm.analysis import (
22
+ chi_statistics,
23
+ compute_metrics,
24
+ count_clusters,
25
+ energy_components,
26
+ energy_conservation_drift,
27
+ interior_mask,
28
+ total_energy,
29
+ void_fraction,
30
+ well_fraction,
31
+ )
32
+ from lfm.config import BoundaryType, FieldLevel, SimulationConfig
33
+ from lfm.constants import (
34
+ ALPHA_EM,
35
+ ALPHA_S,
36
+ CHI0,
37
+ D_ST,
38
+ DT_DEFAULT,
39
+ E_AMPLITUDE_BY_GRID,
40
+ EPSILON_W,
41
+ KAPPA,
42
+ LAMBDA_H,
43
+ N_COLORS,
44
+ N_GENERATIONS,
45
+ OMEGA_LAMBDA,
46
+ OMEGA_MATTER,
47
+ SIN2_THETA_W,
48
+ D,
49
+ )
50
+ from lfm.core.backends import get_backend, gpu_available
51
+ from lfm.core.evolver import Evolver
52
+ from lfm.fields import (
53
+ equilibrate_chi,
54
+ equilibrate_from_fields,
55
+ gaussian_soliton,
56
+ grid_positions,
57
+ place_solitons,
58
+ poisson_solve_fft,
59
+ seed_noise,
60
+ sparse_positions,
61
+ tetrahedral_positions,
62
+ uniform_chi,
63
+ wave_kick,
64
+ )
65
+ from lfm.formulas import (
66
+ mass_table,
67
+ predict_all,
68
+ )
69
+ from lfm.simulation import Simulation
70
+
71
+ __all__ = [
72
+ "__version__",
73
+ # Constants
74
+ "CHI0",
75
+ "D",
76
+ "D_ST",
77
+ "KAPPA",
78
+ "LAMBDA_H",
79
+ "EPSILON_W",
80
+ "ALPHA_S",
81
+ "ALPHA_EM",
82
+ "OMEGA_LAMBDA",
83
+ "OMEGA_MATTER",
84
+ "SIN2_THETA_W",
85
+ "N_COLORS",
86
+ "N_GENERATIONS",
87
+ "DT_DEFAULT",
88
+ "E_AMPLITUDE_BY_GRID",
89
+ # Config
90
+ "SimulationConfig",
91
+ "FieldLevel",
92
+ "BoundaryType",
93
+ # Backends & Simulation
94
+ "Evolver",
95
+ "Simulation",
96
+ "get_backend",
97
+ "gpu_available",
98
+ # Fields
99
+ "gaussian_soliton",
100
+ "place_solitons",
101
+ "wave_kick",
102
+ "poisson_solve_fft",
103
+ "equilibrate_chi",
104
+ "equilibrate_from_fields",
105
+ "seed_noise",
106
+ "uniform_chi",
107
+ "tetrahedral_positions",
108
+ "sparse_positions",
109
+ "grid_positions",
110
+ # Analysis
111
+ "energy_components",
112
+ "total_energy",
113
+ "energy_conservation_drift",
114
+ "chi_statistics",
115
+ "well_fraction",
116
+ "void_fraction",
117
+ "count_clusters",
118
+ "interior_mask",
119
+ "compute_metrics",
120
+ # Formulas
121
+ "predict_all",
122
+ "mass_table",
123
+ ]
@@ -0,0 +1,30 @@
1
+ """Post-processing analysis: structure detection, energy, spectra."""
2
+
3
+ from lfm.analysis.energy import (
4
+ energy_components,
5
+ energy_conservation_drift,
6
+ total_energy,
7
+ )
8
+ from lfm.analysis.metrics import compute_metrics
9
+ from lfm.analysis.structure import (
10
+ chi_statistics,
11
+ count_clusters,
12
+ interior_mask,
13
+ void_fraction,
14
+ well_fraction,
15
+ )
16
+
17
+ __all__ = [
18
+ # energy
19
+ "energy_components",
20
+ "total_energy",
21
+ "energy_conservation_drift",
22
+ # structure
23
+ "chi_statistics",
24
+ "well_fraction",
25
+ "void_fraction",
26
+ "count_clusters",
27
+ "interior_mask",
28
+ # metrics
29
+ "compute_metrics",
30
+ ]
lfm/analysis/energy.py ADDED
@@ -0,0 +1,159 @@
1
+ """
2
+ Energy Diagnostics
3
+ ==================
4
+
5
+ Three-component energy decomposition for LFM fields:
6
+ T = ½(∂Ψ/∂t)² — kinetic energy density
7
+ G = ½c²|∇Ψ|² — gradient energy density
8
+ V = ½χ²|Ψ|² — potential energy density
9
+
10
+ Total energy H = ∫(T + G + V) d³x.
11
+
12
+ Production patterns from exp_sm_02_complex_em_interaction.py and
13
+ lfm_energy_conservation_test.py.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import numpy as np
19
+ from numpy.typing import NDArray
20
+
21
+
22
+ def energy_components(
23
+ psi_r: NDArray[np.floating],
24
+ psi_r_prev: NDArray[np.floating],
25
+ chi: NDArray[np.floating],
26
+ dt: float,
27
+ c: float = 1.0,
28
+ psi_i: NDArray[np.floating] | None = None,
29
+ psi_i_prev: NDArray[np.floating] | None = None,
30
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
31
+ """Compute three-component energy density fields.
32
+
33
+ Supports all field levels:
34
+ - Real E: psi_r shape (N,N,N), psi_i=None
35
+ - Complex Ψ: psi_r & psi_i shape (N,N,N)
36
+ - 3-color Ψₐ: psi_r & psi_i shape (n_colors, N, N, N)
37
+
38
+ Parameters
39
+ ----------
40
+ psi_r : ndarray
41
+ Real part of Ψ, current step.
42
+ psi_r_prev : ndarray
43
+ Real part of Ψ, previous step.
44
+ chi : ndarray, shape (N, N, N)
45
+ χ field at current step.
46
+ dt : float
47
+ Timestep for finite-difference time derivative.
48
+ c : float
49
+ Wave speed (default 1.0).
50
+ psi_i : ndarray or None
51
+ Imaginary part of Ψ, current step.
52
+ psi_i_prev : ndarray or None
53
+ Imaginary part, previous step.
54
+
55
+ Returns
56
+ -------
57
+ kinetic, gradient, potential : ndarray of float64, shape (N, N, N)
58
+ The three energy density components.
59
+ """
60
+ # Time derivative via finite difference
61
+ dpsi_r_dt = (psi_r.astype(np.float64) - psi_r_prev.astype(np.float64)) / dt
62
+
63
+ # Kinetic: ½(∂Ψ/∂t)²
64
+ if psi_r.ndim == 4: # color: (n_colors, N, N, N)
65
+ kinetic = 0.5 * np.sum(dpsi_r_dt**2, axis=0)
66
+ else:
67
+ kinetic = 0.5 * dpsi_r_dt**2
68
+
69
+ if psi_i is not None and psi_i_prev is not None:
70
+ dpsi_i_dt = (psi_i.astype(np.float64) - psi_i_prev.astype(np.float64)) / dt
71
+ if psi_i.ndim == 4:
72
+ kinetic += 0.5 * np.sum(dpsi_i_dt**2, axis=0)
73
+ else:
74
+ kinetic += 0.5 * dpsi_i_dt**2
75
+
76
+ # Gradient: ½c²|∇Ψ|²
77
+ c2 = c**2
78
+
79
+ def _grad_sq(f: NDArray) -> NDArray:
80
+ """Sum of squared gradients over spatial axes."""
81
+ if f.ndim == 4: # (n_colors, N, N, N) → grad each color, sum
82
+ total = np.zeros(f.shape[1:], dtype=np.float64)
83
+ for a in range(f.shape[0]):
84
+ for ax in range(3):
85
+ g = np.gradient(f[a].astype(np.float64), axis=ax)
86
+ total += g**2
87
+ return total
88
+ total = np.zeros(f.shape, dtype=np.float64)
89
+ for ax in range(3):
90
+ g = np.gradient(f.astype(np.float64), axis=ax)
91
+ total += g**2
92
+ return total
93
+
94
+ gradient = 0.5 * c2 * _grad_sq(psi_r)
95
+ if psi_i is not None:
96
+ gradient += 0.5 * c2 * _grad_sq(psi_i)
97
+
98
+ # Potential: ½χ²|Ψ|²
99
+ chi64 = chi.astype(np.float64)
100
+ if psi_r.ndim == 4:
101
+ psi_sq = np.sum(psi_r.astype(np.float64) ** 2, axis=0)
102
+ else:
103
+ psi_sq = psi_r.astype(np.float64) ** 2
104
+ if psi_i is not None:
105
+ if psi_i.ndim == 4:
106
+ psi_sq += np.sum(psi_i.astype(np.float64) ** 2, axis=0)
107
+ else:
108
+ psi_sq += psi_i.astype(np.float64) ** 2
109
+
110
+ potential = 0.5 * chi64**2 * psi_sq
111
+
112
+ return kinetic, gradient, potential
113
+
114
+
115
+ def total_energy(
116
+ psi_r: NDArray[np.floating],
117
+ psi_r_prev: NDArray[np.floating],
118
+ chi: NDArray[np.floating],
119
+ dt: float,
120
+ c: float = 1.0,
121
+ psi_i: NDArray[np.floating] | None = None,
122
+ psi_i_prev: NDArray[np.floating] | None = None,
123
+ ) -> float:
124
+ """Compute total integrated energy H = ∫(T + G + V) d³x.
125
+
126
+ Parameters are the same as :func:`energy_components`.
127
+
128
+ Returns
129
+ -------
130
+ float
131
+ Scalar total energy (sum over all grid points).
132
+ """
133
+ T, G, V = energy_components(
134
+ psi_r, psi_r_prev, chi, dt, c, psi_i, psi_i_prev
135
+ )
136
+ return float(np.sum(T + G + V))
137
+
138
+
139
+ def energy_conservation_drift(
140
+ e_initial: float,
141
+ e_final: float,
142
+ ) -> float:
143
+ """Compute percentage energy drift.
144
+
145
+ Parameters
146
+ ----------
147
+ e_initial : float
148
+ Energy at start of simulation.
149
+ e_final : float
150
+ Energy at end of simulation.
151
+
152
+ Returns
153
+ -------
154
+ float
155
+ |E_final − E_initial| / |E_initial| × 100, or 0 if E_initial ≈ 0.
156
+ """
157
+ if abs(e_initial) < 1e-30:
158
+ return 0.0
159
+ return abs(e_final - e_initial) / abs(e_initial) * 100.0
@@ -0,0 +1,109 @@
1
+ """
2
+ Combined Metrics
3
+ ================
4
+
5
+ All-in-one snapshot metrics matching the production ``compute_metrics()``
6
+ pattern from primordial_soup and universe_simulator.
7
+
8
+ Returns a flat dictionary suitable for logging, JSON, or DataFrame rows.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+ from numpy.typing import NDArray
15
+
16
+ from lfm.analysis.energy import energy_components
17
+ from lfm.analysis.structure import (
18
+ chi_statistics,
19
+ count_clusters,
20
+ void_fraction,
21
+ well_fraction,
22
+ )
23
+
24
+
25
+ def compute_metrics(
26
+ psi_r: NDArray[np.floating],
27
+ psi_r_prev: NDArray[np.floating],
28
+ chi: NDArray[np.floating],
29
+ dt: float,
30
+ c: float = 1.0,
31
+ psi_i: NDArray[np.floating] | None = None,
32
+ psi_i_prev: NDArray[np.floating] | None = None,
33
+ interior_mask: NDArray[np.bool_] | None = None,
34
+ well_threshold: float = 17.0,
35
+ void_threshold: float = 18.0,
36
+ cluster_percentile: float = 90.0,
37
+ ) -> dict[str, float]:
38
+ """Compute a full snapshot of simulation metrics.
39
+
40
+ Parameters
41
+ ----------
42
+ psi_r, psi_r_prev : ndarray
43
+ Real part of Ψ, current and previous steps.
44
+ chi : ndarray, shape (N, N, N)
45
+ The χ field.
46
+ dt : float
47
+ Timestep.
48
+ c : float
49
+ Wave speed.
50
+ psi_i, psi_i_prev : ndarray or None
51
+ Imaginary part of Ψ (None for real fields).
52
+ interior_mask : ndarray of bool or None
53
+ Interior region mask (excludes frozen boundary).
54
+ well_threshold : float
55
+ χ well threshold.
56
+ void_threshold : float
57
+ χ void threshold.
58
+ cluster_percentile : float
59
+ Percentile for cluster detection.
60
+
61
+ Returns
62
+ -------
63
+ dict[str, float]
64
+ Flat dictionary with all metrics.
65
+ """
66
+ # Energy components
67
+ T, G, V = energy_components(
68
+ psi_r, psi_r_prev, chi, dt, c, psi_i, psi_i_prev
69
+ )
70
+ e_kinetic = float(np.sum(T))
71
+ e_gradient = float(np.sum(G))
72
+ e_potential = float(np.sum(V))
73
+ e_total = e_kinetic + e_gradient + e_potential
74
+
75
+ # Chi statistics
76
+ chi_stats = chi_statistics(chi, interior_mask)
77
+
78
+ # Structure
79
+ wells = well_fraction(chi, well_threshold, interior_mask)
80
+ voids = void_fraction(chi, void_threshold, interior_mask)
81
+
82
+ # |Ψ|² for cluster detection
83
+ if psi_r.ndim == 4:
84
+ psi_sq = np.sum(psi_r.astype(np.float64) ** 2, axis=0)
85
+ else:
86
+ psi_sq = psi_r.astype(np.float64) ** 2
87
+ if psi_i is not None:
88
+ if psi_i.ndim == 4:
89
+ psi_sq += np.sum(psi_i.astype(np.float64) ** 2, axis=0)
90
+ else:
91
+ psi_sq += psi_i.astype(np.float64) ** 2
92
+
93
+ clusters = count_clusters(psi_sq, cluster_percentile, interior_mask)
94
+ psi_sq_total = float(np.sum(psi_sq))
95
+
96
+ return {
97
+ "energy_kinetic": e_kinetic,
98
+ "energy_gradient": e_gradient,
99
+ "energy_potential": e_potential,
100
+ "energy_total": e_total,
101
+ "chi_min": chi_stats["min"],
102
+ "chi_max": chi_stats["max"],
103
+ "chi_mean": chi_stats["mean"],
104
+ "chi_std": chi_stats["std"],
105
+ "well_fraction": wells,
106
+ "void_fraction": voids,
107
+ "n_clusters": float(clusters),
108
+ "psi_sq_total": psi_sq_total,
109
+ }
@@ -0,0 +1,165 @@
1
+ """
2
+ Structure Detection
3
+ ===================
4
+
5
+ Detect gravitational wells, voids, and clusters in LFM fields.
6
+
7
+ Production patterns from primordial_soup_v8_four_forces.py:
8
+ - Wells: χ < WELL_THRESHOLD (17.0)
9
+ - Voids: χ > VOID_THRESHOLD (18.0)
10
+ - Clusters: connected components of high |Ψ|² via scipy.ndimage.label
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import numpy as np
16
+ from numpy.typing import NDArray
17
+ from scipy import ndimage
18
+
19
+ from lfm.constants import VOID_THRESHOLD, WELL_THRESHOLD
20
+
21
+
22
+ def chi_statistics(
23
+ chi: NDArray[np.floating],
24
+ interior_mask: NDArray[np.bool_] | None = None,
25
+ ) -> dict[str, float]:
26
+ """Compute χ field statistics.
27
+
28
+ Parameters
29
+ ----------
30
+ chi : ndarray, shape (N, N, N)
31
+ The χ field.
32
+ interior_mask : ndarray of bool or None
33
+ If given, only compute stats where mask is True (excludes boundary).
34
+
35
+ Returns
36
+ -------
37
+ dict with keys: min, max, mean, std
38
+ """
39
+ region = chi[interior_mask] if interior_mask is not None else chi.ravel()
40
+ return {
41
+ "min": float(np.min(region)),
42
+ "max": float(np.max(region)),
43
+ "mean": float(np.mean(region)),
44
+ "std": float(np.std(region)),
45
+ }
46
+
47
+
48
+ def well_fraction(
49
+ chi: NDArray[np.floating],
50
+ threshold: float = WELL_THRESHOLD,
51
+ interior_mask: NDArray[np.bool_] | None = None,
52
+ ) -> float:
53
+ """Fraction of grid points in gravitational wells (χ < threshold).
54
+
55
+ Parameters
56
+ ----------
57
+ chi : ndarray, shape (N, N, N)
58
+ The χ field.
59
+ threshold : float
60
+ Well threshold (default 17.0).
61
+ interior_mask : ndarray of bool or None
62
+ If given, only consider interior points.
63
+
64
+ Returns
65
+ -------
66
+ float
67
+ Fraction in [0, 1].
68
+ """
69
+ region = chi[interior_mask] if interior_mask is not None else chi.ravel()
70
+ if region.size == 0:
71
+ return 0.0
72
+ return float(np.mean(region < threshold))
73
+
74
+
75
+ def void_fraction(
76
+ chi: NDArray[np.floating],
77
+ threshold: float = VOID_THRESHOLD,
78
+ interior_mask: NDArray[np.bool_] | None = None,
79
+ ) -> float:
80
+ """Fraction of grid points in voids (χ > threshold).
81
+
82
+ Parameters
83
+ ----------
84
+ chi : ndarray, shape (N, N, N)
85
+ The χ field.
86
+ threshold : float
87
+ Void threshold (default 18.0).
88
+ interior_mask : ndarray of bool or None
89
+ If given, only consider interior points.
90
+
91
+ Returns
92
+ -------
93
+ float
94
+ Fraction in [0, 1].
95
+ """
96
+ region = chi[interior_mask] if interior_mask is not None else chi.ravel()
97
+ if region.size == 0:
98
+ return 0.0
99
+ return float(np.mean(region > threshold))
100
+
101
+
102
+ def count_clusters(
103
+ field: NDArray[np.floating],
104
+ threshold_percentile: float = 90.0,
105
+ interior_mask: NDArray[np.bool_] | None = None,
106
+ ) -> int:
107
+ """Count connected clusters above a percentile threshold.
108
+
109
+ Uses scipy.ndimage.label on the binary mask (field > percentile).
110
+
111
+ Parameters
112
+ ----------
113
+ field : ndarray, shape (N, N, N)
114
+ Scalar field to cluster (typically |Ψ|²).
115
+ threshold_percentile : float
116
+ Percentile threshold (default 90th) above which points count.
117
+ interior_mask : ndarray of bool or None
118
+ If given, mask out boundary before labeling.
119
+
120
+ Returns
121
+ -------
122
+ int
123
+ Number of connected clusters.
124
+ """
125
+ if interior_mask is not None:
126
+ vals = field[interior_mask]
127
+ else:
128
+ vals = field.ravel()
129
+
130
+ if vals.size == 0:
131
+ return 0
132
+
133
+ thresh = np.percentile(vals, threshold_percentile)
134
+ binary = field > thresh
135
+ if interior_mask is not None:
136
+ binary = binary & interior_mask
137
+
138
+ _, n_clusters = ndimage.label(binary)
139
+ return int(n_clusters)
140
+
141
+
142
+ def interior_mask(
143
+ N: int,
144
+ boundary_fraction: float = 0.3,
145
+ ) -> NDArray[np.bool_]:
146
+ """Create a boolean mask for the interior region (excludes frozen boundary).
147
+
148
+ Parameters
149
+ ----------
150
+ N : int
151
+ Grid size per axis.
152
+ boundary_fraction : float
153
+ Fraction of grid radius for boundary shell.
154
+
155
+ Returns
156
+ -------
157
+ ndarray of bool, shape (N, N, N)
158
+ True in the interior, False in the boundary shell.
159
+ """
160
+ center = N / 2.0
161
+ radius = center * (1.0 - boundary_fraction)
162
+ x = np.arange(N, dtype=np.float64)
163
+ X, Y, Z = np.meshgrid(x, x, x, indexing="ij")
164
+ dist = np.sqrt((X - center) ** 2 + (Y - center) ** 2 + (Z - center) ** 2)
165
+ return dist <= radius