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.
- physbound-0.1.0/.gitignore +31 -0
- physbound-0.1.0/.pre-commit-config.yaml +24 -0
- physbound-0.1.0/.python-version +1 -0
- physbound-0.1.0/LICENSE +21 -0
- physbound-0.1.0/PKG-INFO +147 -0
- physbound-0.1.0/README.md +116 -0
- physbound-0.1.0/pyproject.toml +60 -0
- physbound-0.1.0/smithery.yaml +8 -0
- physbound-0.1.0/src/physbound/__init__.py +3 -0
- physbound-0.1.0/src/physbound/engines/__init__.py +0 -0
- physbound-0.1.0/src/physbound/engines/constants.py +25 -0
- physbound-0.1.0/src/physbound/engines/link_budget.py +217 -0
- physbound-0.1.0/src/physbound/engines/noise.py +125 -0
- physbound-0.1.0/src/physbound/engines/shannon.py +106 -0
- physbound-0.1.0/src/physbound/engines/units.py +103 -0
- physbound-0.1.0/src/physbound/errors.py +34 -0
- physbound-0.1.0/src/physbound/models/__init__.py +0 -0
- physbound-0.1.0/src/physbound/models/common.py +11 -0
- physbound-0.1.0/src/physbound/models/link_budget.py +33 -0
- physbound-0.1.0/src/physbound/models/noise.py +37 -0
- physbound-0.1.0/src/physbound/models/shannon.py +40 -0
- physbound-0.1.0/src/physbound/server.py +279 -0
- physbound-0.1.0/src/physbound/validators.py +70 -0
- physbound-0.1.0/tests/__init__.py +0 -0
- physbound-0.1.0/tests/conftest.py +18 -0
- physbound-0.1.0/tests/test_constants.py +47 -0
- physbound-0.1.0/tests/test_link_budget.py +148 -0
- physbound-0.1.0/tests/test_marketing.py +175 -0
- physbound-0.1.0/tests/test_noise.py +116 -0
- physbound-0.1.0/tests/test_server.py +119 -0
- physbound-0.1.0/tests/test_shannon.py +112 -0
- physbound-0.1.0/tests/test_units.py +111 -0
- physbound-0.1.0/tests/test_validators.py +92 -0
- 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
|
physbound-0.1.0/LICENSE
ADDED
|
@@ -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.
|
physbound-0.1.0/PKG-INFO
ADDED
|
@@ -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)
|
|
37
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[]()
|
|
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)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[]()
|
|
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"]
|
|
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
|
+
}
|