phm-lite-check 1.0.1__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.
- phm_lite_check-1.0.1/LICENSE +21 -0
- phm_lite_check-1.0.1/PKG-INFO +170 -0
- phm_lite_check-1.0.1/README.md +137 -0
- phm_lite_check-1.0.1/phm_lite_check/__init__.py +23 -0
- phm_lite_check-1.0.1/phm_lite_check/checker.py +295 -0
- phm_lite_check-1.0.1/phm_lite_check/integrations/__init__.py +8 -0
- phm_lite_check-1.0.1/phm_lite_check/integrations/autogen_tool.py +74 -0
- phm_lite_check-1.0.1/phm_lite_check/integrations/langchain_tool.py +84 -0
- phm_lite_check-1.0.1/phm_lite_check/mcp_server.py +64 -0
- phm_lite_check-1.0.1/phm_lite_check/robotics.py +201 -0
- phm_lite_check-1.0.1/phm_lite_check.egg-info/PKG-INFO +170 -0
- phm_lite_check-1.0.1/phm_lite_check.egg-info/SOURCES.txt +18 -0
- phm_lite_check-1.0.1/phm_lite_check.egg-info/dependency_links.txt +1 -0
- phm_lite_check-1.0.1/phm_lite_check.egg-info/requires.txt +12 -0
- phm_lite_check-1.0.1/phm_lite_check.egg-info/top_level.txt +1 -0
- phm_lite_check-1.0.1/pyproject.toml +42 -0
- phm_lite_check-1.0.1/setup.cfg +4 -0
- phm_lite_check-1.0.1/tests/test_checker.py +91 -0
- phm_lite_check-1.0.1/tests/test_integrations.py +74 -0
- phm_lite_check-1.0.1/tests/test_robotics.py +107 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PHM Audit Cloud contributors
|
|
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,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phm-lite-check
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Deterministic physics plausibility checking for LLM outputs and robotics datasets (offline subset of the PHM engine). Zero dependencies.
|
|
5
|
+
Author: PHM-Audit
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PHM-Audit/phm-lite-check
|
|
8
|
+
Project-URL: Repository, https://github.com/PHM-Audit/phm-lite-check
|
|
9
|
+
Keywords: llm,agent,validation,physics,hallucination,robotics,dataset-quality,embodied-ai,guardrail
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: langchain
|
|
25
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
26
|
+
Provides-Extra: autogen
|
|
27
|
+
Requires-Dist: autogen-agentchat>=0.4; extra == "autogen"
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# phm-lite-check
|
|
35
|
+
|
|
36
|
+
<!-- mcp-name: io.github.phm-audit/phm-lite-check -->
|
|
37
|
+
|
|
38
|
+
Deterministic physics plausibility checking for **LLM outputs** and
|
|
39
|
+
**robotics / embodied-AI datasets**. Zero dependencies, pure standard
|
|
40
|
+
library, Python >= 3.8.
|
|
41
|
+
|
|
42
|
+
This package is an **independent, community-provided library**: it is the
|
|
43
|
+
offline subset of the PHM physical-judgment engine (proprietary core
|
|
44
|
+
methods are intentionally excluded). A companion plugin
|
|
45
|
+
(`phm-physics-check`) is listed on the Dify Marketplace.
|
|
46
|
+
|
|
47
|
+
> Not affiliated with, endorsed by, or sponsored by LangChain, AutoGen,
|
|
48
|
+
> or Dify. Integration adapters are community-provided.
|
|
49
|
+
|
|
50
|
+
## MCP server
|
|
51
|
+
|
|
52
|
+
This package doubles as a local MCP server (`stdio` transport). After
|
|
53
|
+
installing, point your MCP client at:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
python3 -m phm_lite_check.mcp_server
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Exposing three tools: `physics_text_check`, `physics_quantity_check`,
|
|
60
|
+
`robotics_data_check`.
|
|
61
|
+
|
|
62
|
+
## What it checks
|
|
63
|
+
|
|
64
|
+
### Text / quantities (`check_text`, `full_check`)
|
|
65
|
+
- Dimensional / unit consistency (SI + common units)
|
|
66
|
+
- Universal speed limit (c)
|
|
67
|
+
- Absolute temperature floor (0 K)
|
|
68
|
+
- Non-negativity of mass / length / energy
|
|
69
|
+
- Probability range [0, 1]
|
|
70
|
+
- Kinematic relations (free-fall family, tolerance-checked)
|
|
71
|
+
- Conservation checks (energy / momentum, tolerance-checked)
|
|
72
|
+
|
|
73
|
+
### Robotics records (`check_robotics_record`)
|
|
74
|
+
- Quaternion normalization (|q| within tolerance of 1.0)
|
|
75
|
+
- Timestamp sanity: monotonicity, duplicates, NaN/Inf, outlier gaps
|
|
76
|
+
(possible frame loss)
|
|
77
|
+
- NaN / Inf scan over numeric arrays
|
|
78
|
+
- Unit annotation presence for common motion fields
|
|
79
|
+
|
|
80
|
+
## Design guarantees
|
|
81
|
+
|
|
82
|
+
- **Deterministic**: same input -> same output, always reproducible.
|
|
83
|
+
- **Zero dependencies**: pure standard library; framework adapters import
|
|
84
|
+
their host lazily.
|
|
85
|
+
- **Verdicts**: PASS / WARN / FAIL with structured violation records.
|
|
86
|
+
- **No network calls**: everything runs offline in-process.
|
|
87
|
+
|
|
88
|
+
> NOTE: this package contains only the deterministic offline subset of the
|
|
89
|
+
> PHM physical-judgment engine. Proprietary core methods are intentionally
|
|
90
|
+
> excluded. (Deployment-specific deep checks are available separately via a
|
|
91
|
+
> configurable remote engine endpoint, not included in this package.)
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
**Option A — from the GitHub repo** (no package index needed):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install git+https://github.com/PHM-Audit/phm-lite-check
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Option B — from a Release wheel** (faster, no git clone):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install https://github.com/PHM-Audit/phm-lite-check/releases/download/v1.0.0/phm_lite_check-1.0.0-py3-none-any.whl
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Optional extras** (once installed):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install "phm-lite-check[langchain]" # + langchain-core (adapter tests)
|
|
111
|
+
pip install "phm-lite-check[autogen]" # + autogen-agentchat
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
PyPI listing is planned; both options above work today.
|
|
115
|
+
|
|
116
|
+
## Quickstart — library
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from phm_lite_check import check_text, check_robotics_record
|
|
120
|
+
|
|
121
|
+
check_text("The drone descends at 450 km/h and confidence is 1.8.")
|
|
122
|
+
# {"verdict": "FAIL", "violations": [...], ...}
|
|
123
|
+
|
|
124
|
+
check_robotics_record({
|
|
125
|
+
"quaternion": [1.395, 0.0, 0.0, 0.0], # |q| >> 1 -> FAIL
|
|
126
|
+
"timestamps": [0.0, 0.1, 0.05], # not monotonic -> FAIL
|
|
127
|
+
"position": [1.0, 2.0], # no unit annotated -> WARN
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Quickstart — LangChain
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from phm_lite_check.integrations import langchain_tool
|
|
135
|
+
|
|
136
|
+
tools = langchain_tool.get_langchain_tools() # 3 @tool callables
|
|
137
|
+
agent = create_react_agent(llm, tools) # your usual wiring
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Quickstart — AutoGen (v0.4+)
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
144
|
+
from phm_lite_check.integrations import autogen_tool
|
|
145
|
+
|
|
146
|
+
assistant = AssistantAgent(
|
|
147
|
+
name="checked_assistant",
|
|
148
|
+
model_client=..., # your model client
|
|
149
|
+
tools=autogen_tool.get_autogen_tools(),
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Tools exposed by both adapters
|
|
154
|
+
|
|
155
|
+
| Tool | Input | Checks |
|
|
156
|
+
|------|-------|--------|
|
|
157
|
+
| `physics_text_check` | free text with quantities | unit consistency, limits, probabilities |
|
|
158
|
+
| `physics_quantity_check` | value + unit | one quantity against physical law |
|
|
159
|
+
| `robotics_data_check` | JSON record | quaternions, timestamps, NaN/Inf, units |
|
|
160
|
+
|
|
161
|
+
## Tests
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install -e ".[dev]"
|
|
165
|
+
pytest tests/ -q
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# phm-lite-check
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.phm-audit/phm-lite-check -->
|
|
4
|
+
|
|
5
|
+
Deterministic physics plausibility checking for **LLM outputs** and
|
|
6
|
+
**robotics / embodied-AI datasets**. Zero dependencies, pure standard
|
|
7
|
+
library, Python >= 3.8.
|
|
8
|
+
|
|
9
|
+
This package is an **independent, community-provided library**: it is the
|
|
10
|
+
offline subset of the PHM physical-judgment engine (proprietary core
|
|
11
|
+
methods are intentionally excluded). A companion plugin
|
|
12
|
+
(`phm-physics-check`) is listed on the Dify Marketplace.
|
|
13
|
+
|
|
14
|
+
> Not affiliated with, endorsed by, or sponsored by LangChain, AutoGen,
|
|
15
|
+
> or Dify. Integration adapters are community-provided.
|
|
16
|
+
|
|
17
|
+
## MCP server
|
|
18
|
+
|
|
19
|
+
This package doubles as a local MCP server (`stdio` transport). After
|
|
20
|
+
installing, point your MCP client at:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
python3 -m phm_lite_check.mcp_server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Exposing three tools: `physics_text_check`, `physics_quantity_check`,
|
|
27
|
+
`robotics_data_check`.
|
|
28
|
+
|
|
29
|
+
## What it checks
|
|
30
|
+
|
|
31
|
+
### Text / quantities (`check_text`, `full_check`)
|
|
32
|
+
- Dimensional / unit consistency (SI + common units)
|
|
33
|
+
- Universal speed limit (c)
|
|
34
|
+
- Absolute temperature floor (0 K)
|
|
35
|
+
- Non-negativity of mass / length / energy
|
|
36
|
+
- Probability range [0, 1]
|
|
37
|
+
- Kinematic relations (free-fall family, tolerance-checked)
|
|
38
|
+
- Conservation checks (energy / momentum, tolerance-checked)
|
|
39
|
+
|
|
40
|
+
### Robotics records (`check_robotics_record`)
|
|
41
|
+
- Quaternion normalization (|q| within tolerance of 1.0)
|
|
42
|
+
- Timestamp sanity: monotonicity, duplicates, NaN/Inf, outlier gaps
|
|
43
|
+
(possible frame loss)
|
|
44
|
+
- NaN / Inf scan over numeric arrays
|
|
45
|
+
- Unit annotation presence for common motion fields
|
|
46
|
+
|
|
47
|
+
## Design guarantees
|
|
48
|
+
|
|
49
|
+
- **Deterministic**: same input -> same output, always reproducible.
|
|
50
|
+
- **Zero dependencies**: pure standard library; framework adapters import
|
|
51
|
+
their host lazily.
|
|
52
|
+
- **Verdicts**: PASS / WARN / FAIL with structured violation records.
|
|
53
|
+
- **No network calls**: everything runs offline in-process.
|
|
54
|
+
|
|
55
|
+
> NOTE: this package contains only the deterministic offline subset of the
|
|
56
|
+
> PHM physical-judgment engine. Proprietary core methods are intentionally
|
|
57
|
+
> excluded. (Deployment-specific deep checks are available separately via a
|
|
58
|
+
> configurable remote engine endpoint, not included in this package.)
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
**Option A — from the GitHub repo** (no package index needed):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install git+https://github.com/PHM-Audit/phm-lite-check
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Option B — from a Release wheel** (faster, no git clone):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install https://github.com/PHM-Audit/phm-lite-check/releases/download/v1.0.0/phm_lite_check-1.0.0-py3-none-any.whl
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Optional extras** (once installed):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install "phm-lite-check[langchain]" # + langchain-core (adapter tests)
|
|
78
|
+
pip install "phm-lite-check[autogen]" # + autogen-agentchat
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
PyPI listing is planned; both options above work today.
|
|
82
|
+
|
|
83
|
+
## Quickstart — library
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from phm_lite_check import check_text, check_robotics_record
|
|
87
|
+
|
|
88
|
+
check_text("The drone descends at 450 km/h and confidence is 1.8.")
|
|
89
|
+
# {"verdict": "FAIL", "violations": [...], ...}
|
|
90
|
+
|
|
91
|
+
check_robotics_record({
|
|
92
|
+
"quaternion": [1.395, 0.0, 0.0, 0.0], # |q| >> 1 -> FAIL
|
|
93
|
+
"timestamps": [0.0, 0.1, 0.05], # not monotonic -> FAIL
|
|
94
|
+
"position": [1.0, 2.0], # no unit annotated -> WARN
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Quickstart — LangChain
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from phm_lite_check.integrations import langchain_tool
|
|
102
|
+
|
|
103
|
+
tools = langchain_tool.get_langchain_tools() # 3 @tool callables
|
|
104
|
+
agent = create_react_agent(llm, tools) # your usual wiring
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quickstart — AutoGen (v0.4+)
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
111
|
+
from phm_lite_check.integrations import autogen_tool
|
|
112
|
+
|
|
113
|
+
assistant = AssistantAgent(
|
|
114
|
+
name="checked_assistant",
|
|
115
|
+
model_client=..., # your model client
|
|
116
|
+
tools=autogen_tool.get_autogen_tools(),
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Tools exposed by both adapters
|
|
121
|
+
|
|
122
|
+
| Tool | Input | Checks |
|
|
123
|
+
|------|-------|--------|
|
|
124
|
+
| `physics_text_check` | free text with quantities | unit consistency, limits, probabilities |
|
|
125
|
+
| `physics_quantity_check` | value + unit | one quantity against physical law |
|
|
126
|
+
| `robotics_data_check` | JSON record | quaternions, timestamps, NaN/Inf, units |
|
|
127
|
+
|
|
128
|
+
## Tests
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install -e ".[dev]"
|
|
132
|
+
pytest tests/ -q
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""phm_lite_check -- deterministic physics plausibility checking for
|
|
3
|
+
LLM outputs and robotics datasets. Offline subset of the PHM engine.
|
|
4
|
+
|
|
5
|
+
Public API:
|
|
6
|
+
check_text(text) -> scan text for implausible quantities
|
|
7
|
+
full_check(text, kinematics, conservation) -> text + kinematics + conservation
|
|
8
|
+
check_quantity(value, unit) -> one quantity against physical law
|
|
9
|
+
check_robotics_record(record) -> robotics/embodied-AI dataset record
|
|
10
|
+
ENGINE_ID, __version__
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .checker import (check_text, full_check, check_quantity,
|
|
14
|
+
check_probability, check_kinematics, check_conservation,
|
|
15
|
+
ENGINE_ID, __version__ as _checker_version)
|
|
16
|
+
from .robotics import check_robotics_record
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
__all__ = [
|
|
20
|
+
"check_text", "full_check", "check_quantity", "check_probability",
|
|
21
|
+
"check_kinematics", "check_conservation", "check_robotics_record",
|
|
22
|
+
"ENGINE_ID", "__version__",
|
|
23
|
+
]
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PHM-Lite: Deterministic physics plausibility checker (offline subset).
|
|
4
|
+
|
|
5
|
+
Three-time-calibrated rules from common physical law:
|
|
6
|
+
- Dimensional / unit consistency
|
|
7
|
+
- Universal speed limit (c)
|
|
8
|
+
- Absolute temperature floor (0 K)
|
|
9
|
+
- Non-negativity of mass / length / energy
|
|
10
|
+
- Probability range [0, 1]
|
|
11
|
+
- Kinematic relations (free-fall family, tolerance-checked)
|
|
12
|
+
- Conservation checks (energy / momentum, tolerance-checked)
|
|
13
|
+
|
|
14
|
+
Design guarantees:
|
|
15
|
+
- Deterministic: same input -> same output, always reproducible.
|
|
16
|
+
- Zero dependencies (pure standard library), Python >= 3.8.
|
|
17
|
+
- Verdicts: PASS / WARN / FAIL with structured violation records.
|
|
18
|
+
|
|
19
|
+
NOTE: This is the OFFLINE SUBSET of the PHM physical-judgment engine.
|
|
20
|
+
It intentionally excludes proprietary core methods; deployment-specific
|
|
21
|
+
deep checks are exposed via a configurable remote engine endpoint.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import math
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.0"
|
|
29
|
+
ENGINE_ID = "PHM-Lite"
|
|
30
|
+
DETERMINISTIC = True
|
|
31
|
+
|
|
32
|
+
# --- Physical constants (SI) ---
|
|
33
|
+
C_LIGHT = 299_792_458.0 # m/s, exact
|
|
34
|
+
ABSOLUTE_ZERO_C = -273.15 # degC
|
|
35
|
+
G_EARTH = 9.80665 # m/s^2, standard gravity
|
|
36
|
+
|
|
37
|
+
# Sanity thresholds for WARN (orders of magnitude, engineering judgment)
|
|
38
|
+
WARN_ACC_G = 100.0 # sustained >100 g is suspicious outside impact events
|
|
39
|
+
WARN_SPEED_FRAC = 0.5 # >0.5 c is suspicious for macroscopic objects
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Violation:
|
|
43
|
+
__slots__ = ("rule", "severity", "detail", "location")
|
|
44
|
+
|
|
45
|
+
def __init__(self, rule: str, severity: str, detail: str, location: str = ""):
|
|
46
|
+
self.rule = rule
|
|
47
|
+
self.severity = severity # "FAIL" | "WARN"
|
|
48
|
+
self.detail = detail
|
|
49
|
+
self.location = location
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, str]:
|
|
52
|
+
return {"rule": self.rule, "severity": self.severity,
|
|
53
|
+
"detail": self.detail, "location": self.location}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _verdict(violations: List[Violation]) -> str:
|
|
57
|
+
if any(v.severity == "FAIL" for v in violations):
|
|
58
|
+
return "FAIL"
|
|
59
|
+
if any(v.severity == "WARN" for v in violations):
|
|
60
|
+
return "WARN"
|
|
61
|
+
return "PASS"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------- units ---
|
|
65
|
+
|
|
66
|
+
# unit -> (dimension, to_si_factor, si_offset_for_temperature)
|
|
67
|
+
_UNIT_TABLE = {
|
|
68
|
+
# length
|
|
69
|
+
"m": ("L", 1.0, 0.0), "km": ("L", 1000.0, 0.0), "cm": ("L", 0.01, 0.0),
|
|
70
|
+
"mm": ("L", 0.001, 0.0), "mile": ("L", 1609.344, 0.0),
|
|
71
|
+
"ft": ("L", 0.3048, 0.0), "au": ("L", 1.495978707e11, 0.0),
|
|
72
|
+
"ly": ("L", 9.4607e15, 0.0),
|
|
73
|
+
# time
|
|
74
|
+
"s": ("T", 1.0, 0.0), "ms": ("T", 0.001, 0.0), "min": ("T", 60.0, 0.0),
|
|
75
|
+
"h": ("T", 3600.0, 0.0), "hour": ("T", 3600.0, 0.0),
|
|
76
|
+
"day": ("T", 86400.0, 0.0), "yr": ("T", 3.15576e7, 0.0),
|
|
77
|
+
# mass
|
|
78
|
+
"kg": ("M", 1.0, 0.0), "g": ("M", 0.001, 0.0), "mg": ("M", 1e-6, 0.0),
|
|
79
|
+
"t": ("M", 1000.0, 0.0), "ton": ("M", 1000.0, 0.0),
|
|
80
|
+
"lb": ("M", 0.45359237, 0.0),
|
|
81
|
+
# speed
|
|
82
|
+
"m/s": ("L/T", 1.0, 0.0), "km/h": ("L/T", 1.0 / 3.6, 0.0),
|
|
83
|
+
"km/s": ("L/T", 1000.0, 0.0), "mph": ("L/T", 0.44704, 0.0),
|
|
84
|
+
"knot": ("L/T", 0.514444, 0.0), "kn": ("L/T", 0.514444, 0.0),
|
|
85
|
+
"c": ("L/T", C_LIGHT, 0.0),
|
|
86
|
+
# acceleration
|
|
87
|
+
"m/s2": ("L/T2", 1.0, 0.0), "m/s^2": ("L/T2", 1.0, 0.0),
|
|
88
|
+
"g0": ("L/T2", G_EARTH, 0.0), "gal": ("L/T2", 0.01, 0.0),
|
|
89
|
+
# force / energy / power
|
|
90
|
+
"N": ("M*L/T2", 1.0, 0.0), "kN": ("M*L/T2", 1000.0, 0.0),
|
|
91
|
+
"J": ("M*L2/T2", 1.0, 0.0), "kJ": ("M*L2/T2", 1000.0, 0.0),
|
|
92
|
+
"MJ": ("M*L2/T2", 1e6, 0.0), "Wh": ("M*L2/T2", 3600.0, 0.0),
|
|
93
|
+
"kWh": ("M*L2/T2", 3.6e6, 0.0), "eV": ("M*L2/T2", 1.602176634e-19, 0.0),
|
|
94
|
+
"MeV": ("M*L2/T2", 1.602176634e-13, 0.0),
|
|
95
|
+
"GeV": ("M*L2/T2", 1.602176634e-10, 0.0),
|
|
96
|
+
"W": ("M*L2/T3", 1.0, 0.0), "kW": ("M*L2/T3", 1000.0, 0.0),
|
|
97
|
+
"MW": ("M*L2/T3", 1e6, 0.0), "GW": ("M*L2/T3", 1e9, 0.0),
|
|
98
|
+
"hp": ("M*L2/T3", 745.69987158227, 0.0),
|
|
99
|
+
# temperature (special: offset conversions)
|
|
100
|
+
"K": ("THETA", 1.0, 0.0), "degC": ("THETA", 1.0, 273.15),
|
|
101
|
+
"°C": ("THETA", 1.0, 273.15),
|
|
102
|
+
# pressure
|
|
103
|
+
"Pa": ("M/(L*T2)", 1.0, 0.0), "kPa": ("M/(L*T2)", 1000.0, 0.0),
|
|
104
|
+
"MPa": ("M/(L*T2)", 1e6, 0.0), "bar": ("M/(L*T2)", 1e5, 0.0),
|
|
105
|
+
"atm": ("M/(L*T2)", 101325.0, 0.0),
|
|
106
|
+
# angle
|
|
107
|
+
"deg": ("A", 1.0, 0.0), "rad": ("A", 1.0, 0.0),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_NUM = r"[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?(?:[eE][-+]?\d+)?"
|
|
111
|
+
_QTY_RE = re.compile(
|
|
112
|
+
r"(?P<num>" + _NUM + r")\s?(?P<unit>" +
|
|
113
|
+
"|".join(re.escape(u) for u in sorted(_UNIT_TABLE, key=len, reverse=True)) +
|
|
114
|
+
r")(?![A-Za-z0-9/^])"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def to_si(value: float, unit: str) -> Tuple[Optional[float], Optional[str]]:
|
|
119
|
+
"""Convert a value in a known unit to SI base. Returns (si_value, dim) or (None, None)."""
|
|
120
|
+
u = _UNIT_TABLE.get(unit)
|
|
121
|
+
if u is None:
|
|
122
|
+
return None, None
|
|
123
|
+
dim, factor, offset = u
|
|
124
|
+
v = value * factor + (offset if dim == "THETA" else 0.0)
|
|
125
|
+
return v, dim
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def check_quantity(value: float, unit: str, location: str = "") -> List[Violation]:
|
|
129
|
+
"""Universal per-quantity checks. Applicable to ANY domain."""
|
|
130
|
+
out: List[Violation] = []
|
|
131
|
+
si, dim = to_si(value, unit)
|
|
132
|
+
if si is None:
|
|
133
|
+
return out
|
|
134
|
+
disp = f"{value} {unit}"
|
|
135
|
+
|
|
136
|
+
if dim == "L/T": # speed
|
|
137
|
+
if si < 0:
|
|
138
|
+
out.append(Violation("speed_negative", "FAIL",
|
|
139
|
+
f"Negative speed {disp}", location))
|
|
140
|
+
elif si > C_LIGHT:
|
|
141
|
+
out.append(Violation("speed_exceeds_light", "FAIL",
|
|
142
|
+
f"Speed {disp} exceeds c = 299792458 m/s", location))
|
|
143
|
+
elif si > WARN_SPEED_FRAC * C_LIGHT:
|
|
144
|
+
out.append(Violation("speed_relativistic_warn", "WARN",
|
|
145
|
+
f"Speed {disp} > 0.5c, implausible for macroscopic bodies", location))
|
|
146
|
+
elif dim == "THETA":
|
|
147
|
+
if si < 0:
|
|
148
|
+
out.append(Violation("below_absolute_zero", "FAIL",
|
|
149
|
+
f"Temperature {disp} below absolute zero", location))
|
|
150
|
+
elif dim == "M":
|
|
151
|
+
if si < 0:
|
|
152
|
+
out.append(Violation("negative_mass", "FAIL",
|
|
153
|
+
f"Negative mass {disp}", location))
|
|
154
|
+
elif dim == "L":
|
|
155
|
+
if si < 0:
|
|
156
|
+
out.append(Violation("negative_length", "FAIL",
|
|
157
|
+
f"Negative length {disp}", location))
|
|
158
|
+
elif dim == "L/T2":
|
|
159
|
+
if si < 0:
|
|
160
|
+
out.append(Violation("negative_acceleration_mag", "FAIL",
|
|
161
|
+
f"Negative acceleration magnitude {disp}", location))
|
|
162
|
+
elif si > WARN_ACC_G * G_EARTH:
|
|
163
|
+
out.append(Violation("acceleration_extreme", "WARN",
|
|
164
|
+
f"Acceleration {disp} > {WARN_ACC_G:.0f} g", location))
|
|
165
|
+
elif dim == "M*L/T2":
|
|
166
|
+
if si < 0:
|
|
167
|
+
out.append(Violation("negative_force", "WARN",
|
|
168
|
+
f"Negative force {disp} (check sign convention)", location))
|
|
169
|
+
return out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def check_probability(value: float, location: str = "") -> Optional[Violation]:
|
|
173
|
+
if 0.0 <= value <= 1.0:
|
|
174
|
+
return None
|
|
175
|
+
return Violation("probability_out_of_range", "FAIL",
|
|
176
|
+
f"Probability {value} outside [0, 1]", location)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def check_kinematics(data: Dict[str, float], location: str = "") -> List[Violation]:
|
|
180
|
+
"""Cross-check kinematic descriptions for internal consistency.
|
|
181
|
+
|
|
182
|
+
Accepts any subset of keys (SI units): v0, v, a, t, s (displacement), h (height).
|
|
183
|
+
Verifies v = v0 + a t and s = v0 t + 0.5 a t^2 within tolerance when present.
|
|
184
|
+
"""
|
|
185
|
+
out: List[Violation] = []
|
|
186
|
+
v0, v, a, t = data.get("v0"), data.get("v"), data.get("a"), data.get("t")
|
|
187
|
+
s, h = data.get("s"), data.get("h")
|
|
188
|
+
tol = 0.02 # 2%
|
|
189
|
+
|
|
190
|
+
if None not in (v0, v, a, t) and t != 0:
|
|
191
|
+
pred = v0 + a * t
|
|
192
|
+
if abs(v - pred) > tol * max(abs(v), abs(pred), 1e-12):
|
|
193
|
+
out.append(Violation("kinematics_v_equation", "FAIL",
|
|
194
|
+
f"v = v0 + a t violated: given v={v} m/s, "
|
|
195
|
+
f"predicted {pred:.6g} m/s", location))
|
|
196
|
+
if None not in (v0, a, t, s) and t != 0:
|
|
197
|
+
pred = v0 * t + 0.5 * a * t * t
|
|
198
|
+
if abs(s - pred) > tol * max(abs(s), abs(pred), 1e-12):
|
|
199
|
+
out.append(Violation("kinematics_s_equation", "FAIL",
|
|
200
|
+
f"s = v0 t + a t^2/2 violated: given s={s} m, "
|
|
201
|
+
f"predicted {pred:.6g} m", location))
|
|
202
|
+
if h is not None and t is not None and a is None and t != 0:
|
|
203
|
+
# free-fall consistency: h = 0.5 g t^2 (if user supplied drop height + time)
|
|
204
|
+
pred = 0.5 * G_EARTH * t * t
|
|
205
|
+
if abs(h - pred) > max(tol * max(abs(h), abs(pred), 1e-12), 0.05):
|
|
206
|
+
out.append(Violation("kinematics_freefall", "WARN",
|
|
207
|
+
f"Free-fall check: h={h} m with t={t} s implies "
|
|
208
|
+
f"{pred:.4g} m at standard gravity", location))
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def check_conservation(before: Dict[str, float], after: Dict[str, float],
|
|
213
|
+
location: str = "") -> List[Violation]:
|
|
214
|
+
"""Energy (M*L2/T2) and momentum (M*L/T) conservation with 2% tolerance."""
|
|
215
|
+
out: List[Violation] = []
|
|
216
|
+
tol = 0.02
|
|
217
|
+
for key in ("energy", "momentum"):
|
|
218
|
+
b, a = before.get(key), after.get(key)
|
|
219
|
+
if b is None or a is None:
|
|
220
|
+
continue
|
|
221
|
+
if abs(a - b) > tol * max(abs(b), abs(a), 1e-12):
|
|
222
|
+
out.append(Violation(f"conservation_{key}", "FAIL",
|
|
223
|
+
f"{key.capitalize()} not conserved: {b} -> {a} "
|
|
224
|
+
f"(delta {(a - b) / max(abs(b), 1e-12) * 100:.1f}%)",
|
|
225
|
+
location))
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def check_text(text: str) -> Dict[str, Any]:
|
|
230
|
+
"""Main gate for LLM outputs: extract quantities with known units and check each.
|
|
231
|
+
|
|
232
|
+
Deterministic regex extraction; unknown units are ignored (no false FAILs).
|
|
233
|
+
"""
|
|
234
|
+
violations: List[Violation] = []
|
|
235
|
+
scanned = 0
|
|
236
|
+
for m in _QTY_RE.finditer(text):
|
|
237
|
+
raw_num = m.group("num").replace(",", "")
|
|
238
|
+
unit = m.group("unit")
|
|
239
|
+
try:
|
|
240
|
+
val = float(raw_num)
|
|
241
|
+
except ValueError:
|
|
242
|
+
continue
|
|
243
|
+
# bare 'g' is ambiguous (gram vs gravity); require explicit unit-like usage
|
|
244
|
+
if unit == "g" and not re.search(r"\d\s?g(?:\s|$|[,.;)])", text):
|
|
245
|
+
continue
|
|
246
|
+
loc = text[max(0, m.start() - 24):m.end() + 8].replace("\n", " ")
|
|
247
|
+
violations.extend(check_quantity(val, unit, location=loc))
|
|
248
|
+
scanned += 1
|
|
249
|
+
# probabilities written as "probability/likelihood/confidence 0.xx"
|
|
250
|
+
for m in re.finditer(r"(probability|likelihood|confidence)[^0-9\-+]{0,12}(" + _NUM + r")",
|
|
251
|
+
text, re.I):
|
|
252
|
+
try:
|
|
253
|
+
val = float(m.group(2).replace(",", ""))
|
|
254
|
+
except ValueError:
|
|
255
|
+
continue
|
|
256
|
+
v = check_probability(val, location=m.group(0)[:40])
|
|
257
|
+
if v:
|
|
258
|
+
violations.append(v)
|
|
259
|
+
scanned += 1
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"verdict": _verdict(violations),
|
|
263
|
+
"n_quantities_checked": scanned,
|
|
264
|
+
"violations": [v.to_dict() for v in violations],
|
|
265
|
+
"engine": ENGINE_ID,
|
|
266
|
+
"version": __version__,
|
|
267
|
+
"deterministic": DETERMINISTIC,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def full_check(text: str, kinematics: Optional[Dict[str, float]] = None,
|
|
272
|
+
conservation: Optional[Tuple[Dict[str, float], Dict[str, float]]] = None
|
|
273
|
+
) -> Dict[str, Any]:
|
|
274
|
+
"""Combined check: text scan + optional kinematic + optional conservation."""
|
|
275
|
+
result = check_text(text)
|
|
276
|
+
extra: List[Violation] = []
|
|
277
|
+
if kinematics:
|
|
278
|
+
extra.extend(check_kinematics(kinematics))
|
|
279
|
+
if conservation:
|
|
280
|
+
b, a = conservation
|
|
281
|
+
extra.extend(check_conservation(b, a))
|
|
282
|
+
if extra:
|
|
283
|
+
result["violations"] = result["violations"] + [v.to_dict() for v in extra]
|
|
284
|
+
result["verdict"] = _verdict(
|
|
285
|
+
[Violation(v["rule"], v["severity"], v["detail"], v["location"])
|
|
286
|
+
for v in result["violations"]])
|
|
287
|
+
return result
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
import json
|
|
292
|
+
demo = ("The drone descends at 450 km/h and lands after 12 s. "
|
|
293
|
+
"The battery stores 5 kWh. Confidence: 1.8 probability of success.")
|
|
294
|
+
print(json.dumps(full_check(demo, kinematics={"v0": 0.0, "a": 9.8, "t": 3.0, "v": 50.0}),
|
|
295
|
+
indent=2, ensure_ascii=False))
|