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 +123 -0
- lfm/analysis/__init__.py +30 -0
- lfm/analysis/energy.py +159 -0
- lfm/analysis/metrics.py +109 -0
- lfm/analysis/structure.py +165 -0
- lfm/config.py +165 -0
- lfm/constants.py +179 -0
- lfm/core/__init__.py +5 -0
- lfm/core/backends/__init__.py +78 -0
- lfm/core/backends/cupy_backend.py +203 -0
- lfm/core/backends/kernel_source.py +456 -0
- lfm/core/backends/numpy_backend.py +242 -0
- lfm/core/backends/protocol.py +113 -0
- lfm/core/evolver.py +293 -0
- lfm/core/integrator.py +282 -0
- lfm/core/stencils.py +99 -0
- lfm/fields/__init__.py +28 -0
- lfm/fields/arrangements.py +143 -0
- lfm/fields/equilibrium.py +142 -0
- lfm/fields/random.py +70 -0
- lfm/fields/soliton.py +147 -0
- lfm/formulas/__init__.py +53 -0
- lfm/formulas/masses.py +208 -0
- lfm/formulas/predictions.py +350 -0
- lfm/io/__init__.py +1 -0
- lfm/py.typed +0 -0
- lfm/simulation.py +385 -0
- lfm_physics-0.1.0.dist-info/METADATA +116 -0
- lfm_physics-0.1.0.dist-info/RECORD +31 -0
- lfm_physics-0.1.0.dist-info/WHEEL +4 -0
- lfm_physics-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|
lfm/analysis/__init__.py
ADDED
|
@@ -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
|
lfm/analysis/metrics.py
ADDED
|
@@ -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
|