physbound 0.1.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 (34) hide show
  1. physbound-0.1.0/.gitignore +31 -0
  2. physbound-0.1.0/.pre-commit-config.yaml +24 -0
  3. physbound-0.1.0/.python-version +1 -0
  4. physbound-0.1.0/LICENSE +21 -0
  5. physbound-0.1.0/PKG-INFO +147 -0
  6. physbound-0.1.0/README.md +116 -0
  7. physbound-0.1.0/pyproject.toml +60 -0
  8. physbound-0.1.0/smithery.yaml +8 -0
  9. physbound-0.1.0/src/physbound/__init__.py +3 -0
  10. physbound-0.1.0/src/physbound/engines/__init__.py +0 -0
  11. physbound-0.1.0/src/physbound/engines/constants.py +25 -0
  12. physbound-0.1.0/src/physbound/engines/link_budget.py +217 -0
  13. physbound-0.1.0/src/physbound/engines/noise.py +125 -0
  14. physbound-0.1.0/src/physbound/engines/shannon.py +106 -0
  15. physbound-0.1.0/src/physbound/engines/units.py +103 -0
  16. physbound-0.1.0/src/physbound/errors.py +34 -0
  17. physbound-0.1.0/src/physbound/models/__init__.py +0 -0
  18. physbound-0.1.0/src/physbound/models/common.py +11 -0
  19. physbound-0.1.0/src/physbound/models/link_budget.py +33 -0
  20. physbound-0.1.0/src/physbound/models/noise.py +37 -0
  21. physbound-0.1.0/src/physbound/models/shannon.py +40 -0
  22. physbound-0.1.0/src/physbound/server.py +279 -0
  23. physbound-0.1.0/src/physbound/validators.py +70 -0
  24. physbound-0.1.0/tests/__init__.py +0 -0
  25. physbound-0.1.0/tests/conftest.py +18 -0
  26. physbound-0.1.0/tests/test_constants.py +47 -0
  27. physbound-0.1.0/tests/test_link_budget.py +148 -0
  28. physbound-0.1.0/tests/test_marketing.py +175 -0
  29. physbound-0.1.0/tests/test_noise.py +116 -0
  30. physbound-0.1.0/tests/test_server.py +119 -0
  31. physbound-0.1.0/tests/test_shannon.py +112 -0
  32. physbound-0.1.0/tests/test_units.py +111 -0
  33. physbound-0.1.0/tests/test_validators.py +92 -0
  34. physbound-0.1.0/uv.lock +1818 -0
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Secrets
13
+ .env
14
+ .env.*
15
+ *.key
16
+ *.pem
17
+
18
+ # Tool caches
19
+ .mypy_cache/
20
+ .pytest_cache/
21
+ .ruff_cache/
22
+ .secrets.baseline
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+
28
+ # Private project docs (not for public repo)
29
+ claude.md
30
+ CLAUDE.md
31
+ .claude/
@@ -0,0 +1,24 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: check-added-large-files
6
+ args: ['--maxkb=100']
7
+ - id: check-merge-conflict
8
+ - id: detect-private-key
9
+ - id: check-yaml
10
+ - id: end-of-file-fixer
11
+ - id: trailing-whitespace
12
+
13
+ - repo: https://github.com/Yelp/detect-secrets
14
+ rev: v1.5.0
15
+ hooks:
16
+ - id: detect-secrets
17
+ args: ['--baseline', '.secrets.baseline']
18
+
19
+ - repo: https://github.com/astral-sh/ruff-pre-commit
20
+ rev: v0.9.6
21
+ hooks:
22
+ - id: ruff
23
+ args: [--fix]
24
+ - id: ruff-format
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Jones
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: physbound
3
+ Version: 0.1.0
4
+ Summary: Physical Layer Linter — validates RF and physics calculations against hard physical limits
5
+ Project-URL: Homepage, https://github.com/JonesRobM/physbound
6
+ Project-URL: Repository, https://github.com/JonesRobM/physbound
7
+ Project-URL: Issues, https://github.com/JonesRobM/physbound/issues
8
+ Author-email: Robert Jones <jonesrobm@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Telecommunications Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: fastmcp==2.14.5
19
+ Requires-Dist: numpy==2.2.3
20
+ Requires-Dist: pint==0.25.2
21
+ Requires-Dist: pydantic<3,>=2.10
22
+ Requires-Dist: scipy==1.15.2
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy==1.15.0; extra == 'dev'
25
+ Requires-Dist: pre-commit==4.1.0; extra == 'dev'
26
+ Requires-Dist: pytest-cov==6.0.0; extra == 'dev'
27
+ Requires-Dist: pytest==8.3.5; extra == 'dev'
28
+ Requires-Dist: python-dotenv>=1.1.0; extra == 'dev'
29
+ Requires-Dist: ruff==0.9.6; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # PhysBound
33
+
34
+ **Physical Layer Linter** — An MCP server that validates RF and physics calculations against hard physical limits. Catches AI hallucinations in engineering workflows.
35
+
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
37
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
38
+ [![Tests](https://img.shields.io/badge/tests-107%20passed-brightgreen.svg)]()
39
+
40
+ ---
41
+
42
+ ## What LLMs Get Wrong
43
+
44
+ LLMs routinely hallucinate physics. PhysBound catches it:
45
+
46
+ | # | Category | LLM Hallucination | PhysBound Truth | Verdict |
47
+ |---|----------|-------------------|-----------------|---------|
48
+ | 1 | Shannon-Hartley | "20 MHz 802.11n at 15 dB SNR achieves 500 Mbps" | Shannon limit: **100.6 Mbps** | CAUGHT |
49
+ | 2 | Shannon-Hartley | "100 MHz 5G channel at 20 dB SNR delivers 2 Gbps" | Shannon limit: **665.8 Mbps** | CAUGHT |
50
+ | 3 | Antenna Aperture | "30 cm dish at 1 GHz provides 45 dBi gain" | Aperture limit: **7.4 dBi** | CAUGHT |
51
+ | 4 | Thermal Noise | "Noise floor of -180 dBm/Hz at room temperature" | Actual: **-174.0 dBm/Hz** at 290K | CAUGHT |
52
+ | 5 | Link Budget | "Wi-Fi at 2.4 GHz reaches 10 km at -40 dBm" | Actual RX power: **-94.1 dBm** | CAUGHT |
53
+ | 6 | Link Budget | "1W to GEO with 0 dBi antennas at -80 dBm" | Actual RX power: **-175.1 dBm** | CAUGHT |
54
+
55
+ *Generated automatically by `pytest tests/test_marketing.py -s`*
56
+
57
+ ---
58
+
59
+ ## Quick Start
60
+
61
+ ### Install
62
+
63
+ ```bash
64
+ pip install physbound
65
+ ```
66
+
67
+ ### Use with Claude Desktop
68
+
69
+ Add to your `claude_desktop_config.json`:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "physbound": {
75
+ "command": "uv",
76
+ "args": ["run", "--from", "physbound", "physbound"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ That's it. Claude now has access to physics-validated RF calculations.
83
+
84
+ ---
85
+
86
+ ## Tools
87
+
88
+ ### `rf_link_budget`
89
+
90
+ Computes a full RF link budget using the Friis transmission equation. Validates antenna gains against aperture limits.
91
+
92
+ **Example:** *"What's the received power for a 2.4 GHz link at 100 m with 20 dBm TX, 10 dBi TX gain, 3 dBi RX gain?"*
93
+
94
+ Returns: FSPL, received power, wavelength, and optional aperture limit checks. Rejects antenna gains that violate `G_max = eta * (pi * D / lambda)^2`.
95
+
96
+ ### `shannon_hartley`
97
+
98
+ Computes Shannon-Hartley channel capacity `C = B * log2(1 + SNR)` and validates throughput claims.
99
+
100
+ **Example:** *"Can a 20 MHz channel with 15 dB SNR support 500 Mbps?"*
101
+
102
+ Returns: Theoretical capacity, spectral efficiency, and whether the claim is physically possible. Flags violations with the exact percentage by which the claim exceeds the Shannon limit.
103
+
104
+ ### `noise_floor`
105
+
106
+ Computes thermal noise power `N = k_B * T * B`, cascades noise figures through multi-stage receivers using the Friis noise formula, and calculates receiver sensitivity.
107
+
108
+ **Example:** *"What's the noise floor for a 1 MHz receiver at 290K with a two-stage LNA chain?"*
109
+
110
+ Returns: Thermal noise in dBm and watts, cascaded noise figure, system noise temperature, and receiver sensitivity.
111
+
112
+ ---
113
+
114
+ ## Physics Guarantees
115
+
116
+ Every calculation is validated against hard physical limits:
117
+
118
+ - **Speed of light:** `c = 299,792,458 m/s` — no exceptions
119
+ - **Thermal noise floor:** `N = -174 dBm/Hz` at 290K — the IEEE standard reference
120
+ - **Shannon limit:** `C = B * log2(1 + SNR)` — no throughput claim exceeds this
121
+ - **Aperture limit:** `G_max = eta * (pi * D / lambda)^2` — antenna gain is bounded by physics
122
+
123
+ Violations return structured `PhysicalViolationError` responses with LaTeX explanations, not silent failures.
124
+
125
+ ---
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ # Clone and install
131
+ git clone https://github.com/JonesRobM/physbound.git
132
+ cd physbound
133
+ uv sync --all-extras
134
+
135
+ # Run tests
136
+ uv run pytest tests/ -v
137
+
138
+ # Print hallucination delta table
139
+ uv run pytest tests/test_marketing.py -s
140
+
141
+ # Start MCP server locally
142
+ uv run physbound
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,116 @@
1
+ # PhysBound
2
+
3
+ **Physical Layer Linter** — An MCP server that validates RF and physics calculations against hard physical limits. Catches AI hallucinations in engineering workflows.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
7
+ [![Tests](https://img.shields.io/badge/tests-107%20passed-brightgreen.svg)]()
8
+
9
+ ---
10
+
11
+ ## What LLMs Get Wrong
12
+
13
+ LLMs routinely hallucinate physics. PhysBound catches it:
14
+
15
+ | # | Category | LLM Hallucination | PhysBound Truth | Verdict |
16
+ |---|----------|-------------------|-----------------|---------|
17
+ | 1 | Shannon-Hartley | "20 MHz 802.11n at 15 dB SNR achieves 500 Mbps" | Shannon limit: **100.6 Mbps** | CAUGHT |
18
+ | 2 | Shannon-Hartley | "100 MHz 5G channel at 20 dB SNR delivers 2 Gbps" | Shannon limit: **665.8 Mbps** | CAUGHT |
19
+ | 3 | Antenna Aperture | "30 cm dish at 1 GHz provides 45 dBi gain" | Aperture limit: **7.4 dBi** | CAUGHT |
20
+ | 4 | Thermal Noise | "Noise floor of -180 dBm/Hz at room temperature" | Actual: **-174.0 dBm/Hz** at 290K | CAUGHT |
21
+ | 5 | Link Budget | "Wi-Fi at 2.4 GHz reaches 10 km at -40 dBm" | Actual RX power: **-94.1 dBm** | CAUGHT |
22
+ | 6 | Link Budget | "1W to GEO with 0 dBi antennas at -80 dBm" | Actual RX power: **-175.1 dBm** | CAUGHT |
23
+
24
+ *Generated automatically by `pytest tests/test_marketing.py -s`*
25
+
26
+ ---
27
+
28
+ ## Quick Start
29
+
30
+ ### Install
31
+
32
+ ```bash
33
+ pip install physbound
34
+ ```
35
+
36
+ ### Use with Claude Desktop
37
+
38
+ Add to your `claude_desktop_config.json`:
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "physbound": {
44
+ "command": "uv",
45
+ "args": ["run", "--from", "physbound", "physbound"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ That's it. Claude now has access to physics-validated RF calculations.
52
+
53
+ ---
54
+
55
+ ## Tools
56
+
57
+ ### `rf_link_budget`
58
+
59
+ Computes a full RF link budget using the Friis transmission equation. Validates antenna gains against aperture limits.
60
+
61
+ **Example:** *"What's the received power for a 2.4 GHz link at 100 m with 20 dBm TX, 10 dBi TX gain, 3 dBi RX gain?"*
62
+
63
+ Returns: FSPL, received power, wavelength, and optional aperture limit checks. Rejects antenna gains that violate `G_max = eta * (pi * D / lambda)^2`.
64
+
65
+ ### `shannon_hartley`
66
+
67
+ Computes Shannon-Hartley channel capacity `C = B * log2(1 + SNR)` and validates throughput claims.
68
+
69
+ **Example:** *"Can a 20 MHz channel with 15 dB SNR support 500 Mbps?"*
70
+
71
+ Returns: Theoretical capacity, spectral efficiency, and whether the claim is physically possible. Flags violations with the exact percentage by which the claim exceeds the Shannon limit.
72
+
73
+ ### `noise_floor`
74
+
75
+ Computes thermal noise power `N = k_B * T * B`, cascades noise figures through multi-stage receivers using the Friis noise formula, and calculates receiver sensitivity.
76
+
77
+ **Example:** *"What's the noise floor for a 1 MHz receiver at 290K with a two-stage LNA chain?"*
78
+
79
+ Returns: Thermal noise in dBm and watts, cascaded noise figure, system noise temperature, and receiver sensitivity.
80
+
81
+ ---
82
+
83
+ ## Physics Guarantees
84
+
85
+ Every calculation is validated against hard physical limits:
86
+
87
+ - **Speed of light:** `c = 299,792,458 m/s` — no exceptions
88
+ - **Thermal noise floor:** `N = -174 dBm/Hz` at 290K — the IEEE standard reference
89
+ - **Shannon limit:** `C = B * log2(1 + SNR)` — no throughput claim exceeds this
90
+ - **Aperture limit:** `G_max = eta * (pi * D / lambda)^2` — antenna gain is bounded by physics
91
+
92
+ Violations return structured `PhysicalViolationError` responses with LaTeX explanations, not silent failures.
93
+
94
+ ---
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ # Clone and install
100
+ git clone https://github.com/JonesRobM/physbound.git
101
+ cd physbound
102
+ uv sync --all-extras
103
+
104
+ # Run tests
105
+ uv run pytest tests/ -v
106
+
107
+ # Print hallucination delta table
108
+ uv run pytest tests/test_marketing.py -s
109
+
110
+ # Start MCP server locally
111
+ uv run physbound
112
+ ```
113
+
114
+ ## License
115
+
116
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "physbound"
3
+ version = "0.1.0"
4
+ description = "Physical Layer Linter — validates RF and physics calculations against hard physical limits"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ authors = [
9
+ { name = "Robert Jones", email = "jonesrobm@gmail.com" },
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Science/Research",
14
+ "Intended Audience :: Telecommunications Industry",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Scientific/Engineering :: Physics",
18
+ ]
19
+ dependencies = [
20
+ "fastmcp==2.14.5",
21
+ "pint==0.25.2",
22
+ "scipy==1.15.2",
23
+ "numpy==2.2.3",
24
+ "pydantic>=2.10,<3",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest==8.3.5",
30
+ "pytest-cov==6.0.0",
31
+ "pre-commit==4.1.0",
32
+ "ruff==0.9.6",
33
+ "mypy==1.15.0",
34
+ "python-dotenv>=1.1.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/JonesRobM/physbound"
39
+ Repository = "https://github.com/JonesRobM/physbound"
40
+ Issues = "https://github.com/JonesRobM/physbound/issues"
41
+
42
+ [project.scripts]
43
+ physbound = "physbound.server:main"
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/physbound"]
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py312"
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "I", "UP", "B", "SIM"]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
@@ -0,0 +1,8 @@
1
+ startCommand:
2
+ type: stdio
3
+ configSchema: {}
4
+ commandFunction: |-
5
+ (config) => ({
6
+ command: 'uv',
7
+ args: ['run', '--from', 'physbound', 'physbound']
8
+ })
@@ -0,0 +1,3 @@
1
+ """PhysBound — Physical Layer Linter for AI hallucination detection."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,25 @@
1
+ """Physical constants as Pint quantities wrapping SciPy CODATA values.
2
+
3
+ All downstream modules import the shared UnitRegistry and constants from here.
4
+ No raw floats should leak into engine code — everything carries units.
5
+ """
6
+
7
+ import pint
8
+ from scipy import constants as sc
9
+
10
+ # Single shared registry — import this everywhere
11
+ ureg = pint.UnitRegistry()
12
+ Q_ = ureg.Quantity
13
+
14
+ # Exact physical constants as Pint quantities (CODATA 2018 exact values)
15
+ SPEED_OF_LIGHT = Q_(sc.speed_of_light, "m/s") # 299_792_458 m/s (exact)
16
+ BOLTZMANN = Q_(sc.Boltzmann, "J/K") # 1.380649e-23 J/K (exact)
17
+ PLANCK = Q_(sc.Planck, "J*s") # 6.62607015e-34 J·s (exact)
18
+
19
+ # IEEE standard reference temperature
20
+ T_REF = Q_(290, "K")
21
+
22
+ # Derived: thermal noise floor at T_REF, 1 Hz bandwidth
23
+ # N = k_B * T = 1.380649e-23 * 290 = 4.00388e-21 W/Hz
24
+ # In dBm/Hz: 10 * log10(4.00388e-21 / 1e-3) = -173.977 ≈ -174 dBm/Hz
25
+ THERMAL_NOISE_FLOOR_DBM_PER_HZ = -174.0
@@ -0,0 +1,217 @@
1
+ """RF Link Budget calculator using Friis transmission equation.
2
+
3
+ Formulas:
4
+ FSPL = 20*log10(d) + 20*log10(f) + 20*log10(4*pi/c) (free-space path loss)
5
+ P_rx = P_tx + G_tx + G_rx - FSPL - L_tx - L_rx (Friis transmission)
6
+ G_max = eta * (pi*D/lambda)^2 (aperture limit)
7
+ """
8
+
9
+ import math
10
+
11
+ from physbound.engines.constants import SPEED_OF_LIGHT
12
+ from physbound.engines.units import linear_to_db
13
+ from physbound.errors import PhysicalViolationError
14
+ from physbound.validators import validate_positive_distance, validate_positive_frequency
15
+
16
+ # Default aperture efficiency for parabolic dish antennas
17
+ DEFAULT_APERTURE_EFFICIENCY = 0.55
18
+
19
+
20
+ def free_space_path_loss_db(frequency_hz: float, distance_m: float) -> float:
21
+ """Compute free-space path loss in dB.
22
+
23
+ FSPL(dB) = 20*log10(d) + 20*log10(f) + 20*log10(4*pi/c)
24
+
25
+ Args:
26
+ frequency_hz: Carrier frequency in Hz.
27
+ distance_m: Link distance in meters.
28
+
29
+ Returns:
30
+ FSPL in dB (positive value; a loss).
31
+ """
32
+ validate_positive_frequency(frequency_hz)
33
+ validate_positive_distance(distance_m)
34
+
35
+ c = SPEED_OF_LIGHT.magnitude # m/s
36
+ fspl = (
37
+ 20.0 * math.log10(distance_m)
38
+ + 20.0 * math.log10(frequency_hz)
39
+ + 20.0 * math.log10(4.0 * math.pi / c)
40
+ )
41
+ return fspl
42
+
43
+
44
+ def max_aperture_gain_dbi(
45
+ diameter_m: float,
46
+ frequency_hz: float,
47
+ efficiency: float = DEFAULT_APERTURE_EFFICIENCY,
48
+ ) -> float:
49
+ """Compute maximum antenna gain for a circular aperture.
50
+
51
+ G_max = eta * (pi * D / lambda)^2
52
+
53
+ Args:
54
+ diameter_m: Antenna diameter in meters.
55
+ frequency_hz: Operating frequency in Hz.
56
+ efficiency: Aperture efficiency (default: 0.55 for parabolic dish).
57
+
58
+ Returns:
59
+ Maximum gain in dBi.
60
+ """
61
+ validate_positive_frequency(frequency_hz)
62
+ if diameter_m <= 0:
63
+ raise PhysicalViolationError(
64
+ message=f"Antenna diameter must be positive, got {diameter_m} m",
65
+ law_violated="Antenna Theory",
66
+ latex_explanation=r"$D > 0$ required for a physical antenna aperture",
67
+ claimed_value=diameter_m,
68
+ unit="m",
69
+ )
70
+
71
+ c = SPEED_OF_LIGHT.magnitude
72
+ wavelength = c / frequency_hz
73
+ g_linear = efficiency * (math.pi * diameter_m / wavelength) ** 2
74
+ return linear_to_db(g_linear)
75
+
76
+
77
+ def validate_antenna_gain(
78
+ claimed_gain_dbi: float,
79
+ diameter_m: float,
80
+ frequency_hz: float,
81
+ label: str = "antenna",
82
+ efficiency: float = DEFAULT_APERTURE_EFFICIENCY,
83
+ ) -> float:
84
+ """Validate that claimed antenna gain doesn't exceed the aperture limit.
85
+
86
+ Args:
87
+ claimed_gain_dbi: Claimed antenna gain in dBi.
88
+ diameter_m: Antenna diameter in meters.
89
+ frequency_hz: Operating frequency in Hz.
90
+ label: Human label for error messages (e.g., "TX antenna").
91
+ efficiency: Aperture efficiency (default: 0.55).
92
+
93
+ Returns:
94
+ The computed G_max in dBi.
95
+
96
+ Raises:
97
+ PhysicalViolationError: If claimed gain exceeds G_max.
98
+ """
99
+ g_max = max_aperture_gain_dbi(diameter_m, frequency_hz, efficiency)
100
+
101
+ if claimed_gain_dbi > g_max:
102
+ c = SPEED_OF_LIGHT.magnitude
103
+ wavelength = c / frequency_hz
104
+ raise PhysicalViolationError(
105
+ message=(
106
+ f"{label} claimed gain {claimed_gain_dbi:.1f} dBi exceeds "
107
+ f"aperture limit {g_max:.1f} dBi for {diameter_m} m dish at "
108
+ f"{frequency_hz / 1e9:.3f} GHz"
109
+ ),
110
+ law_violated="Antenna Aperture Limit",
111
+ latex_explanation=(
112
+ rf"$G_{{\max}} = \eta \left(\frac{{\pi D}}{{\lambda}}\right)^2 = "
113
+ rf"{efficiency} \times \left(\frac{{\pi \times {diameter_m}}}"
114
+ rf"{{{wavelength:.4f}}}\right)^2 = {g_max:.1f}\,\text{{dBi}}$. "
115
+ rf"Claimed ${claimed_gain_dbi:.1f}\,\text{{dBi}}$ exceeds this limit."
116
+ ),
117
+ computed_limit=g_max,
118
+ claimed_value=claimed_gain_dbi,
119
+ unit="dBi",
120
+ )
121
+
122
+ return g_max
123
+
124
+
125
+ def compute_link_budget(
126
+ tx_power_dbm: float,
127
+ tx_antenna_gain_dbi: float,
128
+ rx_antenna_gain_dbi: float,
129
+ frequency_hz: float,
130
+ distance_m: float,
131
+ tx_losses_db: float = 0.0,
132
+ rx_losses_db: float = 0.0,
133
+ tx_antenna_diameter_m: float | None = None,
134
+ rx_antenna_diameter_m: float | None = None,
135
+ ) -> dict:
136
+ """Compute a full RF link budget using the Friis transmission equation.
137
+
138
+ P_rx = P_tx + G_tx + G_rx - FSPL - L_tx - L_rx
139
+
140
+ Args:
141
+ tx_power_dbm: Transmit power in dBm.
142
+ tx_antenna_gain_dbi: Transmit antenna gain in dBi.
143
+ rx_antenna_gain_dbi: Receive antenna gain in dBi.
144
+ frequency_hz: Carrier frequency in Hz.
145
+ distance_m: Link distance in meters.
146
+ tx_losses_db: TX-side miscellaneous losses in dB.
147
+ rx_losses_db: RX-side miscellaneous losses in dB.
148
+ tx_antenna_diameter_m: Optional TX antenna diameter for aperture check.
149
+ rx_antenna_diameter_m: Optional RX antenna diameter for aperture check.
150
+
151
+ Returns:
152
+ Dict with FSPL, received power, warnings, human-readable, and LaTeX.
153
+
154
+ Raises:
155
+ PhysicalViolationError: If antenna gains exceed aperture limits.
156
+ """
157
+ warnings = []
158
+ tx_aperture_limit_dbi = None
159
+ rx_aperture_limit_dbi = None
160
+
161
+ # Validate antenna gains against aperture limits if diameters provided
162
+ if tx_antenna_diameter_m is not None:
163
+ tx_aperture_limit_dbi = validate_antenna_gain(
164
+ tx_antenna_gain_dbi, tx_antenna_diameter_m, frequency_hz, "TX antenna"
165
+ )
166
+
167
+ if rx_antenna_diameter_m is not None:
168
+ rx_aperture_limit_dbi = validate_antenna_gain(
169
+ rx_antenna_gain_dbi, rx_antenna_diameter_m, frequency_hz, "RX antenna"
170
+ )
171
+
172
+ # Friis model applicability warning above 300 GHz
173
+ if frequency_hz > 3e11:
174
+ warnings.append(
175
+ f"Frequency {frequency_hz / 1e9:.1f} GHz exceeds 300 GHz; "
176
+ "Friis free-space model may not be accurate due to atmospheric absorption"
177
+ )
178
+
179
+ # Compute FSPL and received power
180
+ fspl = free_space_path_loss_db(frequency_hz, distance_m)
181
+ received_power_dbm = (
182
+ tx_power_dbm + tx_antenna_gain_dbi + rx_antenna_gain_dbi
183
+ - fspl - tx_losses_db - rx_losses_db
184
+ )
185
+
186
+ c = SPEED_OF_LIGHT.magnitude
187
+ wavelength = c / frequency_hz
188
+
189
+ human_readable = (
190
+ f"Link Budget at {frequency_hz / 1e9:.3f} GHz, {distance_m:.1f} m:\n"
191
+ f" TX Power: {tx_power_dbm:.1f} dBm\n"
192
+ f" TX Gain: {tx_antenna_gain_dbi:.1f} dBi\n"
193
+ f" RX Gain: {rx_antenna_gain_dbi:.1f} dBi\n"
194
+ f" FSPL: {fspl:.2f} dB\n"
195
+ f" TX Losses: {tx_losses_db:.1f} dB\n"
196
+ f" RX Losses: {rx_losses_db:.1f} dB\n"
197
+ f" Received Power: {received_power_dbm:.2f} dBm"
198
+ )
199
+
200
+ latex = (
201
+ rf"$P_{{\text{{rx}}}} = P_{{\text{{tx}}}} + G_{{\text{{tx}}}} + G_{{\text{{rx}}}} "
202
+ rf"- \text{{FSPL}} - L_{{\text{{tx}}}} - L_{{\text{{rx}}}} = "
203
+ rf"{tx_power_dbm:.1f} + {tx_antenna_gain_dbi:.1f} + {rx_antenna_gain_dbi:.1f} "
204
+ rf"- {fspl:.2f} - {tx_losses_db:.1f} - {rx_losses_db:.1f} = "
205
+ rf"{received_power_dbm:.2f}\,\text{{dBm}}$"
206
+ )
207
+
208
+ return {
209
+ "fspl_db": fspl,
210
+ "received_power_dbm": received_power_dbm,
211
+ "wavelength_m": wavelength,
212
+ "tx_aperture_limit_dbi": tx_aperture_limit_dbi,
213
+ "rx_aperture_limit_dbi": rx_aperture_limit_dbi,
214
+ "warnings": warnings,
215
+ "human_readable": human_readable,
216
+ "latex": latex,
217
+ }