pmtvs-physics 0.5.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.
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: pmtvs-physics
3
+ Version: 0.5.0
4
+ Summary: Physics analogs for signal manifold analysis
5
+ Author: pmtvs contributors
6
+ License: PolyForm-Strict-1.0.0
7
+ Project-URL: Homepage, https://github.com/pmtvs/pmtvs
8
+ Project-URL: Repository, https://github.com/pmtvs/pmtvs
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: pmtvs-fractal>=0.5.0
21
+ Requires-Dist: numpy>=1.24
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pmtvs-physics"
7
+ version = "0.5.0"
8
+ description = "Physics analogs for signal manifold analysis"
9
+ readme = "README.md"
10
+ license = {text = "PolyForm-Strict-1.0.0"}
11
+ authors = [{name = "pmtvs contributors"}]
12
+ requires-python = ">=3.9"
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: Other/Proprietary License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Scientific/Engineering",
23
+ ]
24
+ dependencies = [
25
+ "pmtvs-fractal>=0.5.0",
26
+ "numpy>=1.24",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=7.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/pmtvs/pmtvs"
36
+ Repository = "https://github.com/pmtvs/pmtvs"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["python"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
@@ -0,0 +1,35 @@
1
+ """
2
+ pmtvs-physics: Physics analogs for signal manifold analysis.
3
+
4
+ Pure math functions. Arrays in, scalars/arrays out.
5
+ No file IO. No orchestration. No pipeline awareness.
6
+
7
+ Novel physics analogs applied to manifold departure:
8
+ Reynolds number → laminar vs turbulent departure
9
+ Phase transitions → thermodynamic melting of geometric order
10
+ Dissipation rate → irreversible geometric structure loss
11
+ """
12
+
13
+ from .viscosity import hurst_viscosity
14
+ from .momentum import (
15
+ compute_momentum,
16
+ compute_reynolds_analog,
17
+ classify_flow_regime,
18
+ compute_dissipation_rate,
19
+ )
20
+ from .thermodynamics import (
21
+ detect_phase_transition,
22
+ free_energy_slope,
23
+ )
24
+
25
+ __all__ = [
26
+ "hurst_viscosity",
27
+ "compute_momentum",
28
+ "compute_reynolds_analog",
29
+ "classify_flow_regime",
30
+ "compute_dissipation_rate",
31
+ "detect_phase_transition",
32
+ "free_energy_slope",
33
+ ]
34
+
35
+ BACKEND = "python"
@@ -0,0 +1,133 @@
1
+ """
2
+ Momentum, Reynolds analog, and dissipation rate
3
+ for manifold departure flow.
4
+ """
5
+
6
+ import numpy as np
7
+
8
+
9
+ def compute_momentum(
10
+ velocity_magnitude: float,
11
+ effective_dim: float,
12
+ ) -> float:
13
+ """
14
+ Momentum analog for manifold departure.
15
+
16
+ momentum = velocity_magnitude × effective_dim
17
+
18
+ effective_dim acts as the "mass" of the system:
19
+ a system moving in higher-dimensional feature space
20
+ has more inertia — harder to stop or redirect.
21
+
22
+ Args:
23
+ velocity_magnitude: speed of departure (L2 norm of
24
+ feature vector velocity)
25
+ effective_dim: participation ratio from eigendecomp
26
+ measures dimensionality of motion
27
+
28
+ Returns:
29
+ float >= 0 — momentum analog
30
+ NaN if inputs are invalid
31
+ """
32
+ v = float(velocity_magnitude)
33
+ m = float(effective_dim)
34
+ if np.isnan(v) or np.isnan(m) or v < 0 or m < 0:
35
+ return float('nan')
36
+ return v * m
37
+
38
+
39
+ def compute_reynolds_analog(
40
+ momentum: float,
41
+ viscosity: float,
42
+ epsilon: float = 1e-10,
43
+ ) -> float:
44
+ """
45
+ Reynolds number analog for manifold departure flow.
46
+
47
+ Re = momentum / viscosity
48
+ = (velocity × effective_dim) / Hurst_departure
49
+
50
+ Interpretation:
51
+ Re >> 1: turbulent departure — likely irreversible
52
+ Re << 1: laminar departure — system may recover
53
+ Re ≈ 1: transitional — critical region
54
+
55
+ Novel: no existing literature applies Reynolds number
56
+ to manifold-geometric system monitoring.
57
+
58
+ Args:
59
+ momentum: momentum analog (from compute_momentum)
60
+ viscosity: viscosity analog (from hurst_viscosity)
61
+ epsilon: floor for viscosity to avoid division by zero
62
+
63
+ Returns:
64
+ float >= 0 — Reynolds analog
65
+ NaN if momentum is NaN or invalid
66
+ """
67
+ m = float(momentum)
68
+ v = float(viscosity)
69
+ if np.isnan(m) or m < 0:
70
+ return float('nan')
71
+ if np.isnan(v) or v < 0:
72
+ v = epsilon
73
+ return m / max(v, epsilon)
74
+
75
+
76
+ def classify_flow_regime(
77
+ reynolds_analog: float,
78
+ laminar_threshold: float = 1.0,
79
+ turbulent_threshold: float = 10.0,
80
+ ) -> str:
81
+ """
82
+ Classify departure flow regime from Reynolds analog.
83
+
84
+ Args:
85
+ reynolds_analog: from compute_reynolds_analog
86
+ laminar_threshold: Re below this → laminar
87
+ turbulent_threshold: Re above this → turbulent
88
+
89
+ Returns:
90
+ 'laminar' | 'transitional' | 'turbulent' | 'unknown'
91
+
92
+ Note: thresholds are initial estimates.
93
+ Calibrate against known outcomes on FD001-004.
94
+ Document calibrated values in CANONICAL_CONFIGS.md.
95
+ """
96
+ re = float(reynolds_analog)
97
+ if np.isnan(re):
98
+ return 'unknown'
99
+ if re <= laminar_threshold:
100
+ return 'laminar'
101
+ if re > turbulent_threshold:
102
+ return 'turbulent'
103
+ return 'transitional'
104
+
105
+
106
+ def compute_dissipation_rate(
107
+ viscosity: float,
108
+ velocity_gradient: float,
109
+ ) -> float:
110
+ """
111
+ Dissipation rate analog.
112
+
113
+ ε = viscosity × velocity_gradient²
114
+
115
+ Classical: ε = ν × (∂u/∂x)² (Navier-Stokes dissipation)
116
+ In Ørthon: ε = Hurst_departure × (ΔV/Δwindow)²
117
+
118
+ Measures how fast the system is losing geometric structure.
119
+ High dissipation = rapid irreversible departure.
120
+
121
+ Args:
122
+ viscosity: Hurst-based viscosity analog [0, 1]
123
+ velocity_gradient: rate of change of velocity magnitude
124
+
125
+ Returns:
126
+ float >= 0 — dissipation rate analog
127
+ NaN if viscosity is invalid
128
+ """
129
+ v = float(viscosity)
130
+ g = float(velocity_gradient)
131
+ if np.isnan(v) or v < 0:
132
+ return float('nan')
133
+ return v * g * g
@@ -0,0 +1,97 @@
1
+ """
2
+ Thermodynamic phase transition detection for manifold departure.
3
+ """
4
+
5
+ import numpy as np
6
+ from typing import Optional
7
+
8
+
9
+ def detect_phase_transition(
10
+ free_energy_series: np.ndarray,
11
+ heat_capacity_series: np.ndarray,
12
+ ) -> dict:
13
+ """
14
+ Detect thermodynamic phase transitions from F = E - TS.
15
+
16
+ Two indicators:
17
+ 1. Free energy sign change:
18
+ F < 0 means entropy dominates energy — geometric
19
+ structure has "melted." In physical systems this
20
+ marks a phase transition.
21
+
22
+ 2. Heat capacity divergence:
23
+ dE/dT → ∞ near critical point (critical slowing down).
24
+ Pre-transition warning: Cv peaks BEFORE F goes negative.
25
+
26
+ Args:
27
+ free_energy_series: time series of F = E - TS values
28
+ heat_capacity_series: time series of dE/dT values
29
+
30
+ Returns:
31
+ dict with keys:
32
+ transition_detected: bool
33
+ transition_window: int or None
34
+ heat_capacity_peak: float
35
+ heat_capacity_peak_window: int or None
36
+ pre_transition_warning: bool
37
+ True if Cv peaks before F sign change —
38
+ early warning of approaching transition
39
+ """
40
+ F = np.asarray(free_energy_series, dtype=np.float64)
41
+ Cv = np.asarray(heat_capacity_series, dtype=np.float64)
42
+
43
+ # Sign change detection — first window where F < 0
44
+ transition_window: Optional[int] = None
45
+ for i in range(len(F)):
46
+ if np.isfinite(F[i]) and F[i] < 0:
47
+ transition_window = i
48
+ break
49
+
50
+ # Heat capacity peak
51
+ finite_cv = [
52
+ (i, float(v)) for i, v in enumerate(Cv)
53
+ if np.isfinite(v)
54
+ ]
55
+ if finite_cv:
56
+ peak_idx, peak_val = max(finite_cv, key=lambda x: x[1])
57
+ else:
58
+ peak_idx, peak_val = None, float('nan')
59
+
60
+ # Pre-transition warning
61
+ pre_warning = (
62
+ peak_idx is not None and
63
+ transition_window is not None and
64
+ peak_idx < transition_window
65
+ )
66
+
67
+ return {
68
+ 'transition_detected': transition_window is not None,
69
+ 'transition_window': transition_window,
70
+ 'heat_capacity_peak': peak_val,
71
+ 'heat_capacity_peak_window': peak_idx,
72
+ 'pre_transition_warning': pre_warning,
73
+ }
74
+
75
+
76
+ def free_energy_slope(free_energy_series: np.ndarray) -> float:
77
+ """
78
+ Trend slope of free energy series.
79
+
80
+ Negative slope → system moving toward phase transition.
81
+ Positive slope → system moving away from transition.
82
+
83
+ Args:
84
+ free_energy_series: 1D array of F = E - TS values
85
+
86
+ Returns:
87
+ float — slope of linear fit
88
+ NaN if fewer than 3 finite values
89
+ """
90
+ F = np.asarray(free_energy_series, dtype=np.float64)
91
+ valid = np.isfinite(F)
92
+ if valid.sum() < 3:
93
+ return float('nan')
94
+ t = np.arange(len(F))[valid].astype(np.float64)
95
+ F_valid = F[valid]
96
+ slope = np.polyfit(t, F_valid, 1)[0]
97
+ return float(slope)
@@ -0,0 +1,35 @@
1
+ """Hurst-based viscosity analog for manifold departure."""
2
+
3
+ import numpy as np
4
+ from pmtvs_fractal import hurst_exponent
5
+
6
+
7
+ def hurst_viscosity(departure_series: np.ndarray) -> float:
8
+ """
9
+ Viscosity analog from Hurst exponent of departure series.
10
+
11
+ Maps the Hurst exponent to a viscosity-like quantity:
12
+ H > 0.5: persistent departure (high viscosity)
13
+ system resists change — hard to reverse
14
+ H < 0.5: antipersistent departure (low viscosity)
15
+ system oscillates — may recover
16
+ H = 0.5: random walk (neutral viscosity)
17
+
18
+ Returns the Hurst exponent directly as the viscosity analog.
19
+ Range: [0, 1].
20
+
21
+ Args:
22
+ departure_series: 1D array of departure distances over time
23
+
24
+ Returns:
25
+ float in [0, 1] — viscosity analog
26
+ NaN if series too short (< 50 observations)
27
+ """
28
+ series = np.asarray(departure_series, dtype=np.float64)
29
+ series = series[np.isfinite(series)]
30
+ if len(series) < 50:
31
+ return float('nan')
32
+ try:
33
+ return float(hurst_exponent(series))
34
+ except Exception:
35
+ return float('nan')
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: pmtvs-physics
3
+ Version: 0.5.0
4
+ Summary: Physics analogs for signal manifold analysis
5
+ Author: pmtvs contributors
6
+ License: PolyForm-Strict-1.0.0
7
+ Project-URL: Homepage, https://github.com/pmtvs/pmtvs
8
+ Project-URL: Repository, https://github.com/pmtvs/pmtvs
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: pmtvs-fractal>=0.5.0
21
+ Requires-Dist: numpy>=1.24
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
@@ -0,0 +1,13 @@
1
+ pyproject.toml
2
+ python/pmtvs_physics/__init__.py
3
+ python/pmtvs_physics/momentum.py
4
+ python/pmtvs_physics/thermodynamics.py
5
+ python/pmtvs_physics/viscosity.py
6
+ python/pmtvs_physics.egg-info/PKG-INFO
7
+ python/pmtvs_physics.egg-info/SOURCES.txt
8
+ python/pmtvs_physics.egg-info/dependency_links.txt
9
+ python/pmtvs_physics.egg-info/requires.txt
10
+ python/pmtvs_physics.egg-info/top_level.txt
11
+ tests/test_momentum.py
12
+ tests/test_thermodynamics.py
13
+ tests/test_viscosity.py
@@ -0,0 +1,5 @@
1
+ pmtvs-fractal>=0.5.0
2
+ numpy>=1.24
3
+
4
+ [dev]
5
+ pytest>=7.0
@@ -0,0 +1 @@
1
+ pmtvs_physics
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,67 @@
1
+ import numpy as np
2
+ import pytest
3
+ from pmtvs_physics import (
4
+ compute_momentum,
5
+ compute_reynolds_analog,
6
+ classify_flow_regime,
7
+ compute_dissipation_rate,
8
+ )
9
+
10
+
11
+ def test_momentum_multiplication():
12
+ assert compute_momentum(5.0, 3.0) == pytest.approx(15.0)
13
+ assert compute_momentum(0.0, 3.0) == pytest.approx(0.0)
14
+ assert compute_momentum(2.5, 4.0) == pytest.approx(10.0)
15
+
16
+
17
+ def test_momentum_nan_on_invalid():
18
+ assert np.isnan(compute_momentum(-1.0, 3.0))
19
+ assert np.isnan(compute_momentum(float('nan'), 3.0))
20
+ assert np.isnan(compute_momentum(5.0, float('nan')))
21
+
22
+
23
+ def test_reynolds_high_momentum_high_re():
24
+ re = compute_reynolds_analog(100.0, 0.5)
25
+ assert re > 10.0, f"High momentum should give Re > 10, got {re:.2f}"
26
+
27
+
28
+ def test_reynolds_high_viscosity_low_re():
29
+ re = compute_reynolds_analog(1.0, 0.9)
30
+ assert re < 2.0, f"High viscosity should give Re < 2, got {re:.2f}"
31
+
32
+
33
+ def test_reynolds_nan_viscosity_uses_epsilon():
34
+ """NaN viscosity falls back to epsilon — should not raise."""
35
+ re = compute_reynolds_analog(5.0, float('nan'))
36
+ assert np.isfinite(re), "NaN viscosity should use epsilon fallback"
37
+ assert re > 0
38
+
39
+
40
+ def test_reynolds_nan_momentum_returns_nan():
41
+ assert np.isnan(compute_reynolds_analog(float('nan'), 0.5))
42
+
43
+
44
+ def test_flow_regime_classification():
45
+ assert classify_flow_regime(0.5) == 'laminar'
46
+ assert classify_flow_regime(5.0) == 'transitional'
47
+ assert classify_flow_regime(50.0) == 'turbulent'
48
+ assert classify_flow_regime(float('nan')) == 'unknown'
49
+
50
+
51
+ def test_flow_regime_boundaries():
52
+ assert classify_flow_regime(1.0) == 'laminar' # at threshold
53
+ assert classify_flow_regime(10.0) == 'transitional' # at threshold
54
+ assert classify_flow_regime(10.1) == 'turbulent'
55
+
56
+
57
+ def test_dissipation_formula():
58
+ d = compute_dissipation_rate(0.7, 2.0)
59
+ assert d == pytest.approx(0.7 * 4.0) # 0.7 × 2² = 2.8
60
+
61
+
62
+ def test_dissipation_zero_gradient():
63
+ assert compute_dissipation_rate(0.7, 0.0) == pytest.approx(0.0)
64
+
65
+
66
+ def test_dissipation_nan_viscosity():
67
+ assert np.isnan(compute_dissipation_rate(float('nan'), 2.0))
@@ -0,0 +1,66 @@
1
+ import numpy as np
2
+ import pytest
3
+ from pmtvs_physics import detect_phase_transition, free_energy_slope
4
+
5
+
6
+ def test_sign_change_detected():
7
+ F = np.array([5.0, 3.0, 1.0, -0.5, -2.0])
8
+ Cv = np.array([0.1, 0.2, 0.5, 0.3, 0.1])
9
+ r = detect_phase_transition(F, Cv)
10
+ assert r['transition_detected']
11
+ assert r['transition_window'] == 3
12
+
13
+
14
+ def test_pre_transition_warning():
15
+ """Cv peaks at window 1, F goes negative at window 3."""
16
+ F = np.array([5.0, 3.0, 1.0, -0.5, -2.0])
17
+ Cv = np.array([0.1, 2.5, 0.3, 0.1, 0.1])
18
+ r = detect_phase_transition(F, Cv)
19
+ assert r['pre_transition_warning']
20
+ assert r['heat_capacity_peak_window'] == 1
21
+ assert r['transition_window'] == 3
22
+
23
+
24
+ def test_no_transition_when_f_positive():
25
+ F = np.array([5.0, 4.0, 3.0, 2.0, 1.0])
26
+ Cv = np.array([0.1, 0.1, 0.1, 0.1, 0.1])
27
+ r = detect_phase_transition(F, Cv)
28
+ assert not r['transition_detected']
29
+ assert r['transition_window'] is None
30
+ assert not r['pre_transition_warning']
31
+
32
+
33
+ def test_immediate_transition():
34
+ """F is negative from the first window."""
35
+ F = np.array([-1.0, -2.0, -3.0])
36
+ Cv = np.array([ 0.1, 0.1, 0.1])
37
+ r = detect_phase_transition(F, Cv)
38
+ assert r['transition_detected']
39
+ assert r['transition_window'] == 0
40
+
41
+
42
+ def test_free_energy_slope_negative():
43
+ """Declining F should have negative slope."""
44
+ F = np.array([5.0, 3.0, 1.0, -1.0, -3.0])
45
+ s = free_energy_slope(F)
46
+ assert s < 0, f"Declining F should have negative slope, got {s}"
47
+
48
+
49
+ def test_free_energy_slope_positive():
50
+ """Rising F should have positive slope."""
51
+ F = np.array([-3.0, -1.0, 1.0, 3.0, 5.0])
52
+ s = free_energy_slope(F)
53
+ assert s > 0, f"Rising F should have positive slope, got {s}"
54
+
55
+
56
+ def test_free_energy_slope_short_series():
57
+ """Fewer than 3 finite values → NaN."""
58
+ assert np.isnan(free_energy_slope(np.array([1.0, 2.0])))
59
+ assert np.isnan(free_energy_slope(np.array([float('nan')] * 5)))
60
+
61
+
62
+ def test_nan_in_free_energy_handled():
63
+ """NaN values in F are skipped for slope computation."""
64
+ F = np.array([5.0, float('nan'), 1.0, float('nan'), -3.0])
65
+ s = free_energy_slope(F)
66
+ assert np.isfinite(s), "Should compute slope on finite values"
@@ -0,0 +1,40 @@
1
+ import numpy as np
2
+ import pytest
3
+ from pmtvs_physics import hurst_viscosity
4
+
5
+
6
+ def test_persistent_series_high_viscosity():
7
+ """Trending series has H close to 1 — high viscosity."""
8
+ series = np.cumsum(np.ones(200))
9
+ v = hurst_viscosity(series)
10
+ assert not np.isnan(v), "Should not be NaN for 200-pt series"
11
+ assert v > 0.7, f"Trend should have H > 0.7, got {v:.3f}"
12
+
13
+
14
+ def test_short_series_returns_nan():
15
+ """Series shorter than 50 observations → NaN."""
16
+ assert np.isnan(hurst_viscosity(np.ones(10)))
17
+ assert np.isnan(hurst_viscosity(np.array([])))
18
+ assert np.isnan(hurst_viscosity(np.ones(49)))
19
+
20
+
21
+ def test_output_in_unit_interval():
22
+ """All valid outputs must be in [0, 1]."""
23
+ rng = np.random.default_rng(42)
24
+ for _ in range(10):
25
+ n = rng.integers(50, 300)
26
+ series = rng.normal(0, 1, n)
27
+ v = hurst_viscosity(series)
28
+ if not np.isnan(v):
29
+ assert 0.0 <= v <= 1.0, f"H={v:.4f} outside [0,1]"
30
+
31
+
32
+ def test_nan_in_series_handled():
33
+ """NaN values in input are stripped before computation."""
34
+ rng = np.random.default_rng(42)
35
+ series = rng.normal(0, 1, 200).tolist()
36
+ series[10] = float('nan')
37
+ series[50] = float('nan')
38
+ v = hurst_viscosity(np.array(series))
39
+ # Should still compute on the 198 finite values
40
+ assert not np.isnan(v)