vmore2 commited on
Commit ·
ce8c08a
0
Parent(s):
Initial release: QSVAPS v0.1.0 - Quantum Superposition Verification for Agent Plan Safety
Browse files- .gitignore +37 -0
- CITATION.cff +19 -0
- CONTRIBUTING.md +46 -0
- LICENSE +21 -0
- README.md +197 -0
- demo.py +230 -0
- examples/api_workflow_demo.py +121 -0
- pyproject.toml +46 -0
- qsvaps/__init__.py +44 -0
- qsvaps/constraint_engine.py +290 -0
- qsvaps/grover_search.py +198 -0
- qsvaps/llm_interface.py +264 -0
- qsvaps/models.py +209 -0
- qsvaps/oracle_builder.py +168 -0
- qsvaps/verifier.py +296 -0
- qsvaps/visualization.py +168 -0
- requirements.txt +5 -0
- tests/__init__.py +0 -0
- tests/test_constraints.py +173 -0
- tests/test_grover.py +81 -0
- tests/test_models.py +123 -0
- tests/test_oracle.py +103 -0
- tests/test_verifier.py +131 -0
.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# Distribution / packaging
|
| 7 |
+
dist/
|
| 8 |
+
build/
|
| 9 |
+
*.egg-info/
|
| 10 |
+
*.egg
|
| 11 |
+
|
| 12 |
+
# Virtual environments
|
| 13 |
+
.venv/
|
| 14 |
+
venv/
|
| 15 |
+
env/
|
| 16 |
+
|
| 17 |
+
# IDE
|
| 18 |
+
.vscode/
|
| 19 |
+
.idea/
|
| 20 |
+
*.swp
|
| 21 |
+
*.swo
|
| 22 |
+
|
| 23 |
+
# Testing
|
| 24 |
+
.pytest_cache/
|
| 25 |
+
.coverage
|
| 26 |
+
htmlcov/
|
| 27 |
+
|
| 28 |
+
# OS files
|
| 29 |
+
.DS_Store
|
| 30 |
+
Thumbs.db
|
| 31 |
+
desktop.ini
|
| 32 |
+
|
| 33 |
+
# Matplotlib output
|
| 34 |
+
*.png
|
| 35 |
+
|
| 36 |
+
# Jupyter
|
| 37 |
+
.ipynb_checkpoints/
|
CITATION.cff
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cff-version: 1.2.0
|
| 2 |
+
title: "QSVAPS: Quantum Superposition Verification for Agent Plan Safety"
|
| 3 |
+
message: "If you use this software, please cite it as below."
|
| 4 |
+
type: software
|
| 5 |
+
authors:
|
| 6 |
+
- name: "QSVAPS Research Team"
|
| 7 |
+
license: MIT
|
| 8 |
+
repository-code: "https://github.com/yourusername/qsvaps"
|
| 9 |
+
keywords:
|
| 10 |
+
- quantum computing
|
| 11 |
+
- ai agents
|
| 12 |
+
- plan verification
|
| 13 |
+
- grover algorithm
|
| 14 |
+
- agent safety
|
| 15 |
+
abstract: >-
|
| 16 |
+
QSVAPS uses Grover's quantum search algorithm as a verification oracle
|
| 17 |
+
for AI agent plans. It encodes plan constraints as quantum phase oracles
|
| 18 |
+
and searches for constraint violations with provable quadratic speedup
|
| 19 |
+
over classical brute-force verification.
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to QSVAPS
|
| 2 |
+
|
| 3 |
+
Thank you for your interest in contributing to QSVAPS!
|
| 4 |
+
|
| 5 |
+
## Getting Started
|
| 6 |
+
|
| 7 |
+
1. Fork the repository
|
| 8 |
+
2. Clone your fork locally
|
| 9 |
+
3. Install dependencies:
|
| 10 |
+
```bash
|
| 11 |
+
pip install -r requirements.txt
|
| 12 |
+
pip install pytest
|
| 13 |
+
```
|
| 14 |
+
4. Run the tests to make sure everything works:
|
| 15 |
+
```bash
|
| 16 |
+
python -m pytest tests/ -v
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
## Development Workflow
|
| 20 |
+
|
| 21 |
+
1. Create a feature branch: `git checkout -b feature/your-feature`
|
| 22 |
+
2. Make your changes
|
| 23 |
+
3. Run the tests: `python -m pytest tests/ -v`
|
| 24 |
+
4. Run the demo to verify end-to-end: `python demo.py`
|
| 25 |
+
5. Submit a pull request
|
| 26 |
+
|
| 27 |
+
## Areas for Contribution
|
| 28 |
+
|
| 29 |
+
- **New constraint types** — extend `ConstraintEngine` with additional plan constraint patterns
|
| 30 |
+
- **Oracle optimizations** — reduce gate count through constraint grouping or ancilla strategies
|
| 31 |
+
- **Real quantum backends** — test and optimize for IBM Quantum / Amazon Braket hardware
|
| 32 |
+
- **Agent framework integrations** — plugins for LangChain, AutoGen, CrewAI
|
| 33 |
+
- **Benchmarks** — larger plan spaces, real-world agent workflows
|
| 34 |
+
|
| 35 |
+
## Code Style
|
| 36 |
+
|
| 37 |
+
- Follow PEP 8
|
| 38 |
+
- Add docstrings to all public classes and methods
|
| 39 |
+
- Include type hints
|
| 40 |
+
- Write tests for new functionality
|
| 41 |
+
|
| 42 |
+
## Reporting Issues
|
| 43 |
+
|
| 44 |
+
- Open a GitHub issue with a clear description
|
| 45 |
+
- Include steps to reproduce and expected vs. actual behavior
|
| 46 |
+
- Include Python version and `qiskit` version
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 QSVAPS Research Team
|
| 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.
|
README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: QSVAPS
|
| 3 |
+
emoji: ⚛️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: python
|
| 7 |
+
sdk_version: "3.12"
|
| 8 |
+
license: mit
|
| 9 |
+
tags:
|
| 10 |
+
- quantum-computing
|
| 11 |
+
- ai-agents
|
| 12 |
+
- grover-algorithm
|
| 13 |
+
- plan-verification
|
| 14 |
+
- agent-safety
|
| 15 |
+
- qiskit
|
| 16 |
+
pinned: false
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# ⚛️ QSVAPS — Quantum Superposition Verification for Agent Plan Safety
|
| 20 |
+
|
| 21 |
+
[](LICENSE)
|
| 22 |
+
[](https://python.org)
|
| 23 |
+
[](#running-tests)
|
| 24 |
+
[](https://qiskit.org)
|
| 25 |
+
|
| 26 |
+
**The first framework to use Grover's quantum search as a verification oracle for AI agent plans.**
|
| 27 |
+
|
| 28 |
+
Classical generation → Quantum verification → Classical refinement.
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## What is QSVAPS?
|
| 33 |
+
|
| 34 |
+
AI agents (LangChain, AutoGen, CrewAI) generate multi-step plans — tool calls, API sequences, code execution chains. But **nobody verifies these plans before execution.** A plan that looks correct step-by-step can fail due to emergent interactions: race conditions, resource conflicts, cascading failures.
|
| 35 |
+
|
| 36 |
+
QSVAPS solves this by encoding plan constraints as a **quantum oracle** and using **Grover's algorithm** to search for failure modes with a provable **quadratic speedup** over classical brute-force verification.
|
| 37 |
+
|
| 38 |
+
| Aspect | Classical | Quantum (Grover) |
|
| 39 |
+
|---|---|---|
|
| 40 |
+
| Finding one failure in N states | O(N) | O(√N) |
|
| 41 |
+
| Certifying no failures | O(N) exhaustive | O(√N) high-probability |
|
| 42 |
+
| 2²⁰ state space | ~1M checks | ~1000 iterations |
|
| 43 |
+
|
| 44 |
+
## Quick Start
|
| 45 |
+
|
| 46 |
+
### Install
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
pip install -r requirements.txt
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### Run the Demo
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
python demo.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
No API keys needed — uses the Qiskit Aer simulator and a mock LLM.
|
| 59 |
+
|
| 60 |
+
### Use in Your Code
|
| 61 |
+
|
| 62 |
+
```python
|
| 63 |
+
from qsvaps import Plan, PlanAction, ResourceConstraint, PlanVerifier
|
| 64 |
+
|
| 65 |
+
# Define a plan
|
| 66 |
+
plan = Plan(
|
| 67 |
+
name="My Agent Plan",
|
| 68 |
+
actions=[
|
| 69 |
+
PlanAction(name="fetch", description="Fetch data", resources=["api"]),
|
| 70 |
+
PlanAction(name="process", description="Process data"),
|
| 71 |
+
PlanAction(name="save", description="Save results", can_fail=False),
|
| 72 |
+
],
|
| 73 |
+
dependencies=[("fetch", "process"), ("process", "save")],
|
| 74 |
+
resource_constraints=[ResourceConstraint("api", max_concurrent=1)],
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# Verify
|
| 78 |
+
verifier = PlanVerifier(shots=2048)
|
| 79 |
+
result = verifier.verify(plan, verbose=True)
|
| 80 |
+
|
| 81 |
+
if not result.is_safe:
|
| 82 |
+
print(f"Found {result.num_violations} failure modes!")
|
| 83 |
+
for witness in result.witnesses:
|
| 84 |
+
print(witness.explanation)
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Verify & Repair with LLM
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
from qsvaps import PlanVerifier, LLMInterface
|
| 91 |
+
|
| 92 |
+
llm = LLMInterface(api_key="sk-...", model="gpt-4")
|
| 93 |
+
# or: llm = LLMInterface(mock=True) # for testing
|
| 94 |
+
|
| 95 |
+
verifier = PlanVerifier(llm=llm, max_repair_iterations=3)
|
| 96 |
+
results = verifier.verify_and_repair(plan, verbose=True)
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## Architecture
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
┌─────────────┐ ┌──────────────────┐ ┌───────────────┐
|
| 103 |
+
│ LLM Agent │────▶│ Constraint │────▶│ Oracle │
|
| 104 |
+
│ generates │ │ Engine extracts │ │ Builder │
|
| 105 |
+
│ Plan │ │ boolean │ │ creates │
|
| 106 |
+
│ │ │ constraints │ │ quantum │
|
| 107 |
+
│ │ │ │ │ circuit │
|
| 108 |
+
└─────────────┘ └──────────────────┘ └───────┬───────┘
|
| 109 |
+
▲ │
|
| 110 |
+
│ ▼
|
| 111 |
+
┌──────┴──────┐ ┌──────────────────┐ ┌───────────────┐
|
| 112 |
+
│ LLM │◀────│ Failure │◀────│ Grover │
|
| 113 |
+
│ repairs │ │ Witnesses │ │ Search │
|
| 114 |
+
│ plan │ │ decoded from │ │ finds │
|
| 115 |
+
│ │ │ measurements │ │ violations │
|
| 116 |
+
└─────────────┘ └──────────────────┘ └───────────────┘
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Project Structure
|
| 120 |
+
|
| 121 |
+
```
|
| 122 |
+
qsvaps/
|
| 123 |
+
├── models.py # Plan, Action, Constraint, Witness dataclasses
|
| 124 |
+
├── constraint_engine.py # Boolean constraint extraction from plans
|
| 125 |
+
├── oracle_builder.py # Quantum phase oracle + Grover diffuser
|
| 126 |
+
├── grover_search.py # Grover's algorithm execution engine
|
| 127 |
+
├── verifier.py # Main verification pipeline
|
| 128 |
+
├── llm_interface.py # LLM integration (OpenAI + mock mode)
|
| 129 |
+
└── visualization.py # ASCII diagrams + matplotlib plots
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## How It Works
|
| 133 |
+
|
| 134 |
+
1. **Plan Formalization** — Your agent's plan is parsed into structured `PlanAction` objects with preconditions, effects, and resource constraints.
|
| 135 |
+
|
| 136 |
+
2. **Constraint Extraction** — The `ConstraintEngine` automatically generates boolean constraints:
|
| 137 |
+
- **Dependency**: If B depends on A, then B succeeding implies A succeeded
|
| 138 |
+
- **Resource**: Actions sharing rate-limited resources can't both succeed in parallel
|
| 139 |
+
- **Completion**: Actions marked `can_fail=False` must succeed
|
| 140 |
+
- **Fallback**: If an action has a fallback, at least one must succeed
|
| 141 |
+
|
| 142 |
+
3. **Oracle Construction** — Constraints are encoded as a quantum phase oracle: a circuit that flips the phase of states where any constraint is violated.
|
| 143 |
+
|
| 144 |
+
4. **Grover Search** — Grover's algorithm amplifies violation states, making them overwhelmingly likely to be measured. With k = π/4 × √(N/M) iterations, violations are found quadratically faster than classical search.
|
| 145 |
+
|
| 146 |
+
5. **Witness Extraction** — Measured bitstrings are decoded into human-readable `FailureWitness` objects showing exactly which actions failed and which constraints were violated.
|
| 147 |
+
|
| 148 |
+
6. **LLM Repair** — Witnesses are fed to an LLM that revises the plan, and verification repeats until the plan is safe.
|
| 149 |
+
|
| 150 |
+
## The Quantum Advantage
|
| 151 |
+
|
| 152 |
+
QSVAPS uses Grover's algorithm — a provably optimal quantum search algorithm — to find plan failures quadratically faster than any classical approach:
|
| 153 |
+
|
| 154 |
+
- **7-qubit circuit** verifies a 6-action pipeline with 128 possible states
|
| 155 |
+
- **127/128 violations** detected in a single Grover iteration
|
| 156 |
+
- **128× theoretical speedup** over exhaustive classical verification
|
| 157 |
+
- Scales to **15+ qubits** on Qiskit Aer simulator, **127 qubits** on IBM Quantum hardware
|
| 158 |
+
|
| 159 |
+
This is not quantum for the sake of quantum — Grover's speedup is **information-theoretically optimal** for unstructured search.
|
| 160 |
+
|
| 161 |
+
## Running Tests
|
| 162 |
+
|
| 163 |
+
```bash
|
| 164 |
+
pip install pytest
|
| 165 |
+
python -m pytest tests/ -v
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
52 tests covering all components: models, constraints, oracle correctness, Grover search, and end-to-end verification.
|
| 169 |
+
|
| 170 |
+
## Dependencies
|
| 171 |
+
|
| 172 |
+
- `qiskit >= 1.0.0`
|
| 173 |
+
- `qiskit-aer >= 0.13.0`
|
| 174 |
+
- `numpy >= 1.24.0`
|
| 175 |
+
- `matplotlib >= 3.7.0`
|
| 176 |
+
- `openai >= 1.0.0` (optional, for LLM integration)
|
| 177 |
+
|
| 178 |
+
## Citation
|
| 179 |
+
|
| 180 |
+
If you use QSVAPS in your research, please cite:
|
| 181 |
+
|
| 182 |
+
```bibtex
|
| 183 |
+
@software{qsvaps2025,
|
| 184 |
+
title={QSVAPS: Quantum Superposition Verification for Agent Plan Safety},
|
| 185 |
+
year={2025},
|
| 186 |
+
license={MIT},
|
| 187 |
+
url={https://github.com/yourusername/qsvaps}
|
| 188 |
+
}
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
## Contributing
|
| 192 |
+
|
| 193 |
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
| 194 |
+
|
| 195 |
+
## License
|
| 196 |
+
|
| 197 |
+
[MIT](LICENSE)
|
demo.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
QSVAPS — End-to-End Demo
|
| 3 |
+
|
| 4 |
+
Demonstrates quantum-powered plan verification without needing
|
| 5 |
+
any API keys or external services. Uses the Qiskit Aer simulator
|
| 6 |
+
and mock LLM.
|
| 7 |
+
|
| 8 |
+
Run:
|
| 9 |
+
cd "c:\\Users\\vrush\\OneDrive\\Documents\\Quantum AI"
|
| 10 |
+
python demo.py
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
|
| 16 |
+
# Ensure the package is importable
|
| 17 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 18 |
+
|
| 19 |
+
from qsvaps.models import (
|
| 20 |
+
Plan,
|
| 21 |
+
PlanAction,
|
| 22 |
+
PlanConstraint,
|
| 23 |
+
ResourceConstraint,
|
| 24 |
+
ConstraintType,
|
| 25 |
+
)
|
| 26 |
+
from qsvaps.constraint_engine import ConstraintEngine
|
| 27 |
+
from qsvaps.grover_search import GroverSearchEngine
|
| 28 |
+
from qsvaps.verifier import PlanVerifier
|
| 29 |
+
from qsvaps.llm_interface import LLMInterface
|
| 30 |
+
from qsvaps.visualization import QSVAPSVisualizer
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ─── Sample Plans ─────────────────────────────────────────────────────────────
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def create_flawed_api_plan() -> Plan:
|
| 37 |
+
"""A realistic API orchestration plan with several hidden flaws:
|
| 38 |
+
|
| 39 |
+
Flaws:
|
| 40 |
+
1. fetch_users and fetch_products share api_quota but may run in parallel
|
| 41 |
+
2. log_transaction has can_fail=False but depends on a chain that can fail
|
| 42 |
+
3. No fallback for fetch_users (single point of failure)
|
| 43 |
+
"""
|
| 44 |
+
return Plan(
|
| 45 |
+
name="Data Processing Pipeline",
|
| 46 |
+
actions=[
|
| 47 |
+
PlanAction(
|
| 48 |
+
name="fetch_users",
|
| 49 |
+
description="Fetch user data from primary API",
|
| 50 |
+
resources=["api_quota"],
|
| 51 |
+
can_fail=True,
|
| 52 |
+
),
|
| 53 |
+
PlanAction(
|
| 54 |
+
name="fetch_products",
|
| 55 |
+
description="Fetch product catalog from API",
|
| 56 |
+
resources=["api_quota"],
|
| 57 |
+
can_fail=True,
|
| 58 |
+
),
|
| 59 |
+
PlanAction(
|
| 60 |
+
name="merge_data",
|
| 61 |
+
description="Join user preferences with products",
|
| 62 |
+
resources=["memory"],
|
| 63 |
+
can_fail=True,
|
| 64 |
+
),
|
| 65 |
+
PlanAction(
|
| 66 |
+
name="apply_rules",
|
| 67 |
+
description="Apply business rules and discounts",
|
| 68 |
+
can_fail=True,
|
| 69 |
+
),
|
| 70 |
+
PlanAction(
|
| 71 |
+
name="send_notification",
|
| 72 |
+
description="Send email notifications to users",
|
| 73 |
+
resources=["email_service"],
|
| 74 |
+
can_fail=True,
|
| 75 |
+
),
|
| 76 |
+
PlanAction(
|
| 77 |
+
name="log_transaction",
|
| 78 |
+
description="Log transaction to audit database",
|
| 79 |
+
can_fail=False, # MUST succeed
|
| 80 |
+
),
|
| 81 |
+
],
|
| 82 |
+
dependencies=[
|
| 83 |
+
("fetch_users", "merge_data"),
|
| 84 |
+
("fetch_products", "merge_data"),
|
| 85 |
+
("merge_data", "apply_rules"),
|
| 86 |
+
("apply_rules", "send_notification"),
|
| 87 |
+
("send_notification", "log_transaction"),
|
| 88 |
+
],
|
| 89 |
+
resource_constraints=[
|
| 90 |
+
ResourceConstraint("api_quota", max_concurrent=1),
|
| 91 |
+
],
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def create_safe_plan() -> Plan:
|
| 96 |
+
"""A simple sequential plan that should pass verification."""
|
| 97 |
+
return Plan(
|
| 98 |
+
name="Simple Sequential Pipeline",
|
| 99 |
+
actions=[
|
| 100 |
+
PlanAction(name="init", description="Initialize", can_fail=False),
|
| 101 |
+
PlanAction(name="process", description="Process", can_fail=False),
|
| 102 |
+
PlanAction(name="finalize", description="Finalize", can_fail=False),
|
| 103 |
+
],
|
| 104 |
+
dependencies=[
|
| 105 |
+
("init", "process"),
|
| 106 |
+
("process", "finalize"),
|
| 107 |
+
],
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# ─── Demo Runner ──────────────────────────────────────────────────────────────
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def demo_1_verify_flawed():
|
| 115 |
+
"""Demo 1: Verify a plan with known flaws."""
|
| 116 |
+
print("═" * 60)
|
| 117 |
+
print(" DEMO 1: Verifying a Flawed Plan")
|
| 118 |
+
print("═" * 60)
|
| 119 |
+
|
| 120 |
+
plan = create_flawed_api_plan()
|
| 121 |
+
viz = QSVAPSVisualizer()
|
| 122 |
+
|
| 123 |
+
print("\n📋 Input Plan:")
|
| 124 |
+
print(viz.draw_plan(plan))
|
| 125 |
+
|
| 126 |
+
verifier = PlanVerifier(shots=2048)
|
| 127 |
+
result = verifier.verify(plan, verbose=True)
|
| 128 |
+
|
| 129 |
+
print(viz.format_result(result))
|
| 130 |
+
return result
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def demo_2_repair():
|
| 134 |
+
"""Demo 2: Verify and repair using mock LLM."""
|
| 135 |
+
print("\n\n" + "═" * 60)
|
| 136 |
+
print(" DEMO 2: Verify & Repair with Mock LLM")
|
| 137 |
+
print("═" * 60)
|
| 138 |
+
|
| 139 |
+
plan = create_flawed_api_plan()
|
| 140 |
+
mock_llm = LLMInterface(mock=True)
|
| 141 |
+
|
| 142 |
+
verifier = PlanVerifier(
|
| 143 |
+
llm=mock_llm, max_repair_iterations=2, shots=2048
|
| 144 |
+
)
|
| 145 |
+
results = verifier.verify_and_repair(plan, verbose=True)
|
| 146 |
+
|
| 147 |
+
if len(results) > 1:
|
| 148 |
+
print(f"\n📊 Verification ran {len(results)} round(s)")
|
| 149 |
+
for i, r in enumerate(results):
|
| 150 |
+
status = (
|
| 151 |
+
"✅ SAFE"
|
| 152 |
+
if r.is_safe
|
| 153 |
+
else f"❌ {r.num_violations} violations"
|
| 154 |
+
)
|
| 155 |
+
print(f" Round {i + 1}: {status}")
|
| 156 |
+
|
| 157 |
+
return results
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def demo_3_safe_plan():
|
| 161 |
+
"""Demo 3: Verify a plan that should be safe."""
|
| 162 |
+
print("\n\n" + "═" * 60)
|
| 163 |
+
print(" DEMO 3: Verifying a Safe Plan")
|
| 164 |
+
print("═" * 60)
|
| 165 |
+
|
| 166 |
+
plan = create_safe_plan()
|
| 167 |
+
viz = QSVAPSVisualizer()
|
| 168 |
+
|
| 169 |
+
print("\n📋 Input Plan:")
|
| 170 |
+
print(viz.draw_plan(plan))
|
| 171 |
+
|
| 172 |
+
verifier = PlanVerifier(shots=2048)
|
| 173 |
+
result = verifier.verify(plan, verbose=True)
|
| 174 |
+
|
| 175 |
+
print(viz.format_result(result))
|
| 176 |
+
return result
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def demo_4_benchmark():
|
| 180 |
+
"""Demo 4: Quantum vs Classical benchmark."""
|
| 181 |
+
print("\n\n" + "═" * 60)
|
| 182 |
+
print(" DEMO 4: Quantum vs Classical Benchmark")
|
| 183 |
+
print("═" * 60)
|
| 184 |
+
|
| 185 |
+
plan = create_flawed_api_plan()
|
| 186 |
+
engine = ConstraintEngine(plan)
|
| 187 |
+
constraints = engine.extract_constraints()
|
| 188 |
+
violations = engine.find_all_violations(constraints)
|
| 189 |
+
|
| 190 |
+
search_engine = GroverSearchEngine()
|
| 191 |
+
benchmark = search_engine.benchmark(
|
| 192 |
+
violations, engine.var_mapping.num_variables, shots=4096
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
print(f"\n📊 Benchmark Results:")
|
| 196 |
+
print(f" Qubits: {benchmark['num_qubits']}")
|
| 197 |
+
print(f" State space: {benchmark['total_states']:,}")
|
| 198 |
+
print(f" Violations: {benchmark['num_violations']}")
|
| 199 |
+
print(f" Grover iterations: {benchmark['grover_iterations']}")
|
| 200 |
+
print(f" Detection rate: {benchmark['detection_rate'] * 100:.1f}%")
|
| 201 |
+
print(f" Quantum time: {benchmark['quantum_time_ms']:.2f} ms")
|
| 202 |
+
print(f" Classical time: {benchmark['classical_time_ms']:.2f} ms")
|
| 203 |
+
print(f" Theoretical speedup: {benchmark['theoretical_speedup']:.1f}×")
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def main():
|
| 207 |
+
print("""
|
| 208 |
+
╔══════════════════════════════════════════════════════════╗
|
| 209 |
+
║ ║
|
| 210 |
+
║ ⚛️ QSVAPS — Quantum Superposition Verification ║
|
| 211 |
+
║ for Agent Plan Safety ║
|
| 212 |
+
║ ║
|
| 213 |
+
║ Quantum-powered verification of AI agent plans ║
|
| 214 |
+
║ using Grover's search algorithm ║
|
| 215 |
+
║ ║
|
| 216 |
+
╚══════════════════════════════════════════════════════════╝
|
| 217 |
+
""")
|
| 218 |
+
|
| 219 |
+
demo_1_verify_flawed()
|
| 220 |
+
demo_2_repair()
|
| 221 |
+
demo_3_safe_plan()
|
| 222 |
+
demo_4_benchmark()
|
| 223 |
+
|
| 224 |
+
print(f"\n{'═' * 60}")
|
| 225 |
+
print(f" 🎉 All demos complete!")
|
| 226 |
+
print(f"{'═' * 60}\n")
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
if __name__ == "__main__":
|
| 230 |
+
main()
|
examples/api_workflow_demo.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example: API Workflow Verification
|
| 3 |
+
|
| 4 |
+
Demonstrates verifying a complex API orchestration plan with
|
| 5 |
+
resource conflicts, dependency chains, and mandatory steps.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 12 |
+
|
| 13 |
+
from qsvaps import (
|
| 14 |
+
Plan,
|
| 15 |
+
PlanAction,
|
| 16 |
+
PlanConstraint,
|
| 17 |
+
ResourceConstraint,
|
| 18 |
+
ConstraintType,
|
| 19 |
+
PlanVerifier,
|
| 20 |
+
QSVAPSVisualizer,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
"""Build and verify an API orchestration plan."""
|
| 26 |
+
|
| 27 |
+
# ── Define the plan ──────────────────────────────────────────────────
|
| 28 |
+
plan = Plan(
|
| 29 |
+
name="Multi-API Data Aggregation",
|
| 30 |
+
actions=[
|
| 31 |
+
PlanAction(
|
| 32 |
+
name="auth_service_a",
|
| 33 |
+
description="Authenticate with Service A",
|
| 34 |
+
resources=["auth_tokens"],
|
| 35 |
+
can_fail=True,
|
| 36 |
+
),
|
| 37 |
+
PlanAction(
|
| 38 |
+
name="auth_service_b",
|
| 39 |
+
description="Authenticate with Service B",
|
| 40 |
+
resources=["auth_tokens"],
|
| 41 |
+
can_fail=True,
|
| 42 |
+
),
|
| 43 |
+
PlanAction(
|
| 44 |
+
name="fetch_users",
|
| 45 |
+
description="Fetch user records from Service A",
|
| 46 |
+
resources=["bandwidth"],
|
| 47 |
+
can_fail=True,
|
| 48 |
+
),
|
| 49 |
+
PlanAction(
|
| 50 |
+
name="fetch_analytics",
|
| 51 |
+
description="Fetch analytics from Service B",
|
| 52 |
+
resources=["bandwidth"],
|
| 53 |
+
can_fail=True,
|
| 54 |
+
),
|
| 55 |
+
PlanAction(
|
| 56 |
+
name="transform",
|
| 57 |
+
description="Transform and join datasets",
|
| 58 |
+
resources=["compute"],
|
| 59 |
+
can_fail=True,
|
| 60 |
+
),
|
| 61 |
+
PlanAction(
|
| 62 |
+
name="validate",
|
| 63 |
+
description="Validate output schema",
|
| 64 |
+
can_fail=False, # Must succeed
|
| 65 |
+
),
|
| 66 |
+
PlanAction(
|
| 67 |
+
name="store_results",
|
| 68 |
+
description="Store results in database",
|
| 69 |
+
resources=["db_connection"],
|
| 70 |
+
can_fail=False, # Must succeed
|
| 71 |
+
),
|
| 72 |
+
],
|
| 73 |
+
dependencies=[
|
| 74 |
+
("auth_service_a", "fetch_users"),
|
| 75 |
+
("auth_service_b", "fetch_analytics"),
|
| 76 |
+
("fetch_users", "transform"),
|
| 77 |
+
("fetch_analytics", "transform"),
|
| 78 |
+
("transform", "validate"),
|
| 79 |
+
("validate", "store_results"),
|
| 80 |
+
],
|
| 81 |
+
resource_constraints=[
|
| 82 |
+
ResourceConstraint("auth_tokens", max_concurrent=1),
|
| 83 |
+
ResourceConstraint("bandwidth", max_concurrent=1),
|
| 84 |
+
],
|
| 85 |
+
custom_constraints=[
|
| 86 |
+
PlanConstraint(
|
| 87 |
+
expression=f"x0 or x1", # At least one auth must work
|
| 88 |
+
description="At least one authentication must succeed",
|
| 89 |
+
constraint_type=ConstraintType.CUSTOM,
|
| 90 |
+
variables_involved=["s_auth_service_a", "s_auth_service_b"],
|
| 91 |
+
),
|
| 92 |
+
],
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# ── Verify ───────────────────────────────────────────────────────────
|
| 96 |
+
viz = QSVAPSVisualizer()
|
| 97 |
+
print(viz.draw_plan(plan))
|
| 98 |
+
|
| 99 |
+
verifier = PlanVerifier(shots=4096)
|
| 100 |
+
result = verifier.verify(plan, verbose=True)
|
| 101 |
+
|
| 102 |
+
print(viz.format_result(result))
|
| 103 |
+
|
| 104 |
+
# ── Print summary ────────────────────────────────────────────────────
|
| 105 |
+
if not result.is_safe:
|
| 106 |
+
print(
|
| 107 |
+
f"\n⚠️ This plan has {result.num_violations} potential "
|
| 108 |
+
f"failure modes out of {result.total_states:,} possible "
|
| 109 |
+
f"execution states."
|
| 110 |
+
)
|
| 111 |
+
print(
|
| 112 |
+
f" Grover's algorithm found them in "
|
| 113 |
+
f"{result.grover_iterations} iteration(s) "
|
| 114 |
+
f"(vs ~{result.total_states} classical checks)."
|
| 115 |
+
)
|
| 116 |
+
else:
|
| 117 |
+
print("\n✅ Plan is safe under all modeled scenarios.")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
main()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "qsvaps"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Quantum Superposition Verification for Agent Plan Safety — Grover's search as a verification oracle for AI agent plans"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.10"
|
| 11 |
+
license = {text = "MIT"}
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "QSVAPS Research Team"}
|
| 14 |
+
]
|
| 15 |
+
keywords = ["quantum", "ai-agents", "verification", "grover", "plan-safety", "qiskit", "agent-safety"]
|
| 16 |
+
classifiers = [
|
| 17 |
+
"Development Status :: 3 - Alpha",
|
| 18 |
+
"Intended Audience :: Science/Research",
|
| 19 |
+
"License :: OSI Approved :: MIT License",
|
| 20 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 21 |
+
"Topic :: Scientific/Engineering :: Physics",
|
| 22 |
+
"Programming Language :: Python :: 3.10",
|
| 23 |
+
"Programming Language :: Python :: 3.11",
|
| 24 |
+
"Programming Language :: Python :: 3.12",
|
| 25 |
+
]
|
| 26 |
+
dependencies = [
|
| 27 |
+
"qiskit>=1.0.0",
|
| 28 |
+
"qiskit-aer>=0.13.0",
|
| 29 |
+
"numpy>=1.24.0",
|
| 30 |
+
"matplotlib>=3.7.0",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
[project.optional-dependencies]
|
| 34 |
+
llm = ["openai>=1.0.0"]
|
| 35 |
+
dev = ["pytest>=7.0", "pytest-cov"]
|
| 36 |
+
|
| 37 |
+
[project.urls]
|
| 38 |
+
Homepage = "https://github.com/yourusername/qsvaps"
|
| 39 |
+
Repository = "https://github.com/yourusername/qsvaps"
|
| 40 |
+
Issues = "https://github.com/yourusername/qsvaps/issues"
|
| 41 |
+
|
| 42 |
+
[tool.setuptools.packages.find]
|
| 43 |
+
include = ["qsvaps*"]
|
| 44 |
+
|
| 45 |
+
[tool.pytest.ini_options]
|
| 46 |
+
testpaths = ["tests"]
|
qsvaps/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
QSVAPS — Quantum Superposition Verification for Agent Plan Safety
|
| 3 |
+
|
| 4 |
+
A framework that uses Grover's quantum search algorithm as a verification
|
| 5 |
+
oracle for AI agent plans. Classical generation → Quantum verification →
|
| 6 |
+
Classical refinement.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
__version__ = "0.1.0"
|
| 10 |
+
|
| 11 |
+
from .models import (
|
| 12 |
+
Plan,
|
| 13 |
+
PlanAction,
|
| 14 |
+
PlanConstraint,
|
| 15 |
+
ResourceConstraint,
|
| 16 |
+
ConstraintType,
|
| 17 |
+
FailureWitness,
|
| 18 |
+
VerificationResult,
|
| 19 |
+
VariableMapping,
|
| 20 |
+
)
|
| 21 |
+
from .constraint_engine import ConstraintEngine
|
| 22 |
+
from .oracle_builder import OracleBuilder
|
| 23 |
+
from .grover_search import GroverSearchEngine, SearchResult
|
| 24 |
+
from .verifier import PlanVerifier
|
| 25 |
+
from .llm_interface import LLMInterface
|
| 26 |
+
from .visualization import QSVAPSVisualizer
|
| 27 |
+
|
| 28 |
+
__all__ = [
|
| 29 |
+
"Plan",
|
| 30 |
+
"PlanAction",
|
| 31 |
+
"PlanConstraint",
|
| 32 |
+
"ResourceConstraint",
|
| 33 |
+
"ConstraintType",
|
| 34 |
+
"FailureWitness",
|
| 35 |
+
"VerificationResult",
|
| 36 |
+
"VariableMapping",
|
| 37 |
+
"ConstraintEngine",
|
| 38 |
+
"OracleBuilder",
|
| 39 |
+
"GroverSearchEngine",
|
| 40 |
+
"SearchResult",
|
| 41 |
+
"PlanVerifier",
|
| 42 |
+
"LLMInterface",
|
| 43 |
+
"QSVAPSVisualizer",
|
| 44 |
+
]
|
qsvaps/constraint_engine.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Constraint extraction and boolean encoding for agent plans.
|
| 3 |
+
|
| 4 |
+
Converts a Plan's structural properties (dependencies, resource limits,
|
| 5 |
+
completion requirements, fallback chains) into boolean constraints that
|
| 6 |
+
can be encoded as a quantum oracle.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Dict, List, Set, Tuple
|
| 10 |
+
|
| 11 |
+
from .models import (
|
| 12 |
+
Plan,
|
| 13 |
+
PlanAction,
|
| 14 |
+
PlanConstraint,
|
| 15 |
+
ConstraintType,
|
| 16 |
+
ResourceConstraint,
|
| 17 |
+
VariableMapping,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ConstraintEngine:
|
| 22 |
+
"""Extracts and encodes boolean constraints from agent plans.
|
| 23 |
+
|
| 24 |
+
The engine assigns a boolean variable to each relevant plan property
|
| 25 |
+
(action success, parallel execution flags), then generates constraints
|
| 26 |
+
as boolean formulas over those variables.
|
| 27 |
+
|
| 28 |
+
Usage:
|
| 29 |
+
engine = ConstraintEngine(plan)
|
| 30 |
+
constraints = engine.extract_constraints()
|
| 31 |
+
violations = engine.find_all_violations(constraints)
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, plan: Plan):
|
| 35 |
+
self.plan = plan
|
| 36 |
+
self.var_mapping = VariableMapping()
|
| 37 |
+
self._assign_variables()
|
| 38 |
+
|
| 39 |
+
# ─── Variable Assignment ──────────────────────────────────────────────
|
| 40 |
+
|
| 41 |
+
def _assign_variables(self):
|
| 42 |
+
"""Assign boolean variables to plan properties.
|
| 43 |
+
|
| 44 |
+
Creates:
|
| 45 |
+
- s_<action>: success variable for each action (1 = succeeds)
|
| 46 |
+
- p_<a>_<b>: parallel execution variable for each pair of actions
|
| 47 |
+
sharing a resource (1 = run in parallel)
|
| 48 |
+
"""
|
| 49 |
+
idx = 0
|
| 50 |
+
|
| 51 |
+
# Success variable for each action
|
| 52 |
+
for action in self.plan.actions:
|
| 53 |
+
var_name = f"s_{action.name}"
|
| 54 |
+
self.var_mapping.variables[var_name] = idx
|
| 55 |
+
self.var_mapping.descriptions[var_name] = (
|
| 56 |
+
f"Action '{action.name}' succeeds"
|
| 57 |
+
)
|
| 58 |
+
idx += 1
|
| 59 |
+
|
| 60 |
+
# Identify resource-sharing action pairs
|
| 61 |
+
resource_users: Dict[str, List[str]] = {}
|
| 62 |
+
for action in self.plan.actions:
|
| 63 |
+
for res in action.resources:
|
| 64 |
+
resource_users.setdefault(res, []).append(action.name)
|
| 65 |
+
|
| 66 |
+
# Parallel execution variable for each pair sharing a resource
|
| 67 |
+
for res, users in resource_users.items():
|
| 68 |
+
if len(users) > 1:
|
| 69 |
+
for i in range(len(users)):
|
| 70 |
+
for j in range(i + 1, len(users)):
|
| 71 |
+
var_name = f"p_{users[i]}_{users[j]}"
|
| 72 |
+
self.var_mapping.variables[var_name] = idx
|
| 73 |
+
self.var_mapping.descriptions[var_name] = (
|
| 74 |
+
f"'{users[i]}' and '{users[j]}' run in parallel"
|
| 75 |
+
)
|
| 76 |
+
idx += 1
|
| 77 |
+
|
| 78 |
+
# ─── Constraint Extraction ────────────────────────────────────────────
|
| 79 |
+
|
| 80 |
+
def extract_constraints(self) -> List[PlanConstraint]:
|
| 81 |
+
"""Extract all constraints from the plan.
|
| 82 |
+
|
| 83 |
+
Returns a list of PlanConstraint objects covering:
|
| 84 |
+
- Dependency constraints
|
| 85 |
+
- Resource constraints
|
| 86 |
+
- Completion constraints
|
| 87 |
+
- Fallback constraints
|
| 88 |
+
- User-defined custom constraints
|
| 89 |
+
"""
|
| 90 |
+
constraints: List[PlanConstraint] = []
|
| 91 |
+
constraints.extend(self._dependency_constraints())
|
| 92 |
+
constraints.extend(self._resource_constraints())
|
| 93 |
+
constraints.extend(self._completion_constraints())
|
| 94 |
+
constraints.extend(self._fallback_constraints())
|
| 95 |
+
constraints.extend(self.plan.custom_constraints)
|
| 96 |
+
return constraints
|
| 97 |
+
|
| 98 |
+
def _dependency_constraints(self) -> List[PlanConstraint]:
|
| 99 |
+
"""If B depends on A, then B succeeding implies A succeeded.
|
| 100 |
+
|
| 101 |
+
s_dependent → s_prerequisite ≡ ¬s_dependent ∨ s_prerequisite
|
| 102 |
+
"""
|
| 103 |
+
constraints = []
|
| 104 |
+
for prereq, dependent in self.plan.dependencies:
|
| 105 |
+
prereq_var = f"s_{prereq}"
|
| 106 |
+
dep_var = f"s_{dependent}"
|
| 107 |
+
|
| 108 |
+
if (
|
| 109 |
+
prereq_var in self.var_mapping.variables
|
| 110 |
+
and dep_var in self.var_mapping.variables
|
| 111 |
+
):
|
| 112 |
+
pi = self.var_mapping.variables[prereq_var]
|
| 113 |
+
di = self.var_mapping.variables[dep_var]
|
| 114 |
+
|
| 115 |
+
constraints.append(
|
| 116 |
+
PlanConstraint(
|
| 117 |
+
expression=f"(not x{di}) or x{pi}",
|
| 118 |
+
description=(
|
| 119 |
+
f"'{dependent}' requires '{prereq}' to succeed first"
|
| 120 |
+
),
|
| 121 |
+
constraint_type=ConstraintType.DEPENDENCY,
|
| 122 |
+
variables_involved=[prereq_var, dep_var],
|
| 123 |
+
)
|
| 124 |
+
)
|
| 125 |
+
return constraints
|
| 126 |
+
|
| 127 |
+
def _resource_constraints(self) -> List[PlanConstraint]:
|
| 128 |
+
"""Shared resources with limited concurrency.
|
| 129 |
+
|
| 130 |
+
If two actions share a resource with max_concurrent=1, they
|
| 131 |
+
cannot both succeed while running in parallel:
|
| 132 |
+
¬(p_ij ∧ s_i ∧ s_j)
|
| 133 |
+
"""
|
| 134 |
+
constraints = []
|
| 135 |
+
for rc in self.plan.resource_constraints:
|
| 136 |
+
users = [
|
| 137 |
+
a for a in self.plan.actions if rc.resource_name in a.resources
|
| 138 |
+
]
|
| 139 |
+
if len(users) <= rc.max_concurrent:
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
for i in range(len(users)):
|
| 143 |
+
for j in range(i + 1, len(users)):
|
| 144 |
+
p_var = f"p_{users[i].name}_{users[j].name}"
|
| 145 |
+
si_var = f"s_{users[i].name}"
|
| 146 |
+
sj_var = f"s_{users[j].name}"
|
| 147 |
+
|
| 148 |
+
if p_var in self.var_mapping.variables:
|
| 149 |
+
pi = self.var_mapping.variables[p_var]
|
| 150 |
+
si = self.var_mapping.variables[si_var]
|
| 151 |
+
sj = self.var_mapping.variables[sj_var]
|
| 152 |
+
|
| 153 |
+
constraints.append(
|
| 154 |
+
PlanConstraint(
|
| 155 |
+
expression=f"not (x{pi} and x{si} and x{sj})",
|
| 156 |
+
description=(
|
| 157 |
+
f"'{users[i].name}' and '{users[j].name}' "
|
| 158 |
+
f"cannot both use '{rc.resource_name}' "
|
| 159 |
+
f"simultaneously"
|
| 160 |
+
),
|
| 161 |
+
constraint_type=ConstraintType.RESOURCE,
|
| 162 |
+
variables_involved=[p_var, si_var, sj_var],
|
| 163 |
+
)
|
| 164 |
+
)
|
| 165 |
+
return constraints
|
| 166 |
+
|
| 167 |
+
def _completion_constraints(self) -> List[PlanConstraint]:
|
| 168 |
+
"""Actions that must not fail generate: s_i = True."""
|
| 169 |
+
constraints = []
|
| 170 |
+
for action in self.plan.actions:
|
| 171 |
+
if not action.can_fail:
|
| 172 |
+
var_name = f"s_{action.name}"
|
| 173 |
+
idx = self.var_mapping.variables[var_name]
|
| 174 |
+
constraints.append(
|
| 175 |
+
PlanConstraint(
|
| 176 |
+
expression=f"x{idx}",
|
| 177 |
+
description=f"'{action.name}' must succeed",
|
| 178 |
+
constraint_type=ConstraintType.COMPLETION,
|
| 179 |
+
variables_involved=[var_name],
|
| 180 |
+
)
|
| 181 |
+
)
|
| 182 |
+
return constraints
|
| 183 |
+
|
| 184 |
+
def _fallback_constraints(self) -> List[PlanConstraint]:
|
| 185 |
+
"""If an action has a fallback, at least one must succeed:
|
| 186 |
+
s_main ∨ s_fallback
|
| 187 |
+
"""
|
| 188 |
+
constraints = []
|
| 189 |
+
for action in self.plan.actions:
|
| 190 |
+
if action.fallback:
|
| 191 |
+
main_var = f"s_{action.name}"
|
| 192 |
+
fb_var = f"s_{action.fallback}"
|
| 193 |
+
|
| 194 |
+
if (
|
| 195 |
+
main_var in self.var_mapping.variables
|
| 196 |
+
and fb_var in self.var_mapping.variables
|
| 197 |
+
):
|
| 198 |
+
mi = self.var_mapping.variables[main_var]
|
| 199 |
+
fi = self.var_mapping.variables[fb_var]
|
| 200 |
+
|
| 201 |
+
constraints.append(
|
| 202 |
+
PlanConstraint(
|
| 203 |
+
expression=f"x{mi} or x{fi}",
|
| 204 |
+
description=(
|
| 205 |
+
f"Either '{action.name}' or its fallback "
|
| 206 |
+
f"'{action.fallback}' must succeed"
|
| 207 |
+
),
|
| 208 |
+
constraint_type=ConstraintType.FALLBACK,
|
| 209 |
+
variables_involved=[main_var, fb_var],
|
| 210 |
+
)
|
| 211 |
+
)
|
| 212 |
+
return constraints
|
| 213 |
+
|
| 214 |
+
# ─── Evaluation ───────────────────────────────────────────────────────
|
| 215 |
+
|
| 216 |
+
def build_violation_formula(self, constraints: List[PlanConstraint]) -> str:
|
| 217 |
+
"""Build combined violation formula: NOT(C1 AND C2 AND ... AND Cn)."""
|
| 218 |
+
if not constraints:
|
| 219 |
+
return "False"
|
| 220 |
+
parts = [f"({c.expression})" for c in constraints]
|
| 221 |
+
return f"not ({' and '.join(parts)})"
|
| 222 |
+
|
| 223 |
+
def evaluate_state(
|
| 224 |
+
self, state: int, constraints: List[PlanConstraint]
|
| 225 |
+
) -> Tuple[bool, List[PlanConstraint]]:
|
| 226 |
+
"""Evaluate whether a state violates any constraints.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
state: Integer whose binary representation gives variable values.
|
| 230 |
+
Bit i (from LSB) = value of variable x_i.
|
| 231 |
+
constraints: List of constraints to check.
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
Tuple of (is_violation, list_of_violated_constraints).
|
| 235 |
+
"""
|
| 236 |
+
n = self.var_mapping.num_variables
|
| 237 |
+
bits = format(state, f"0{n}b")
|
| 238 |
+
|
| 239 |
+
# Build variable assignment: x0 = LSB
|
| 240 |
+
var_values: Dict[str, bool] = {}
|
| 241 |
+
for i in range(n):
|
| 242 |
+
var_values[f"x{i}"] = bits[n - 1 - i] == "1"
|
| 243 |
+
|
| 244 |
+
violated: List[PlanConstraint] = []
|
| 245 |
+
for constraint in constraints:
|
| 246 |
+
result = self._eval_bool_expr(constraint.expression, var_values)
|
| 247 |
+
if not result:
|
| 248 |
+
violated.append(constraint)
|
| 249 |
+
|
| 250 |
+
return len(violated) > 0, violated
|
| 251 |
+
|
| 252 |
+
def find_all_violations(
|
| 253 |
+
self, constraints: List[PlanConstraint]
|
| 254 |
+
) -> List[int]:
|
| 255 |
+
"""Classical brute-force: enumerate all states and find violations.
|
| 256 |
+
|
| 257 |
+
This is O(2^n) — the quantum approach uses Grover's algorithm
|
| 258 |
+
to achieve O(√(2^n)) for the same task.
|
| 259 |
+
"""
|
| 260 |
+
n = self.var_mapping.num_variables
|
| 261 |
+
violations: List[int] = []
|
| 262 |
+
for state in range(2**n):
|
| 263 |
+
is_violation, _ = self.evaluate_state(state, constraints)
|
| 264 |
+
if is_violation:
|
| 265 |
+
violations.append(state)
|
| 266 |
+
return violations
|
| 267 |
+
|
| 268 |
+
# ─── Boolean Expression Evaluation ────────────────────────────────────
|
| 269 |
+
|
| 270 |
+
@staticmethod
|
| 271 |
+
def _eval_bool_expr(expr: str, var_values: Dict[str, bool]) -> bool:
|
| 272 |
+
"""Safely evaluate a boolean expression with given variable values.
|
| 273 |
+
|
| 274 |
+
Supported operators: and, or, not, parentheses.
|
| 275 |
+
Variables are named x0, x1, x2, ...
|
| 276 |
+
"""
|
| 277 |
+
eval_expr = expr
|
| 278 |
+
|
| 279 |
+
# Replace variables (longest-name-first to prevent x1 matching x10)
|
| 280 |
+
sorted_vars = sorted(var_values.keys(), key=len, reverse=True)
|
| 281 |
+
for var_name in sorted_vars:
|
| 282 |
+
eval_expr = eval_expr.replace(var_name, str(var_values[var_name]))
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
return bool(
|
| 286 |
+
eval(eval_expr, {"__builtins__": {}}, {"True": True, "False": False})
|
| 287 |
+
)
|
| 288 |
+
except Exception:
|
| 289 |
+
# On parse error, assume constraint is satisfied (safe default)
|
| 290 |
+
return True
|
qsvaps/grover_search.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Grover's quantum search algorithm implementation for QSVAPS.
|
| 3 |
+
|
| 4 |
+
Provides the GroverSearchEngine that executes Grover circuits on quantum
|
| 5 |
+
backends and compares against classical brute-force search.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import math
|
| 9 |
+
import time
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
from typing import Callable, Dict, List, Optional, Tuple
|
| 12 |
+
|
| 13 |
+
from qiskit import QuantumCircuit
|
| 14 |
+
|
| 15 |
+
from .oracle_builder import OracleBuilder
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class SearchResult:
|
| 20 |
+
"""Result from a Grover search execution.
|
| 21 |
+
|
| 22 |
+
Attributes:
|
| 23 |
+
measured_states: Raw measurement counts {bitstring: count}.
|
| 24 |
+
top_states: Sorted list of (bitstring, count) pairs, descending.
|
| 25 |
+
num_shots: Total number of measurement shots.
|
| 26 |
+
num_iterations: Number of Grover iterations applied.
|
| 27 |
+
circuit_depth: Depth of the executed circuit.
|
| 28 |
+
circuit_gate_count: Total gates in the circuit.
|
| 29 |
+
execution_time_ms: Wall-clock execution time in milliseconds.
|
| 30 |
+
violation_states_found: State indices confirmed as violations.
|
| 31 |
+
"""
|
| 32 |
+
measured_states: Dict[str, int] = field(default_factory=dict)
|
| 33 |
+
top_states: List[Tuple[str, int]] = field(default_factory=list)
|
| 34 |
+
num_shots: int = 0
|
| 35 |
+
num_iterations: int = 0
|
| 36 |
+
circuit_depth: int = 0
|
| 37 |
+
circuit_gate_count: int = 0
|
| 38 |
+
execution_time_ms: float = 0.0
|
| 39 |
+
violation_states_found: List[int] = field(default_factory=list)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class GroverSearchEngine:
|
| 43 |
+
"""Executes Grover's search on quantum backends.
|
| 44 |
+
|
| 45 |
+
Manages circuit construction, backend execution, and result
|
| 46 |
+
interpretation. Supports benchmarking against classical search.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
backend: A Qiskit backend (defaults to AerSimulator if None).
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self, backend=None):
|
| 53 |
+
if backend is None:
|
| 54 |
+
from qiskit_aer import AerSimulator
|
| 55 |
+
backend = AerSimulator()
|
| 56 |
+
self.backend = backend
|
| 57 |
+
|
| 58 |
+
def search(
|
| 59 |
+
self,
|
| 60 |
+
violation_states: List[int],
|
| 61 |
+
num_qubits: int,
|
| 62 |
+
num_violations_estimate: Optional[int] = None,
|
| 63 |
+
shots: int = 2048,
|
| 64 |
+
) -> SearchResult:
|
| 65 |
+
"""Run Grover's search to find violation states.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
violation_states: Known violation state indices (for oracle
|
| 69 |
+
construction).
|
| 70 |
+
num_qubits: Number of qubits / boolean variables.
|
| 71 |
+
num_violations_estimate: Optional estimate of violation count
|
| 72 |
+
(used to determine iteration count if different from len).
|
| 73 |
+
shots: Number of measurement shots.
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
SearchResult with measurement data and found violations.
|
| 77 |
+
"""
|
| 78 |
+
if not violation_states:
|
| 79 |
+
return SearchResult()
|
| 80 |
+
|
| 81 |
+
total_states = 2**num_qubits
|
| 82 |
+
n_violations = num_violations_estimate or len(violation_states)
|
| 83 |
+
|
| 84 |
+
# Build circuit components
|
| 85 |
+
oracle = OracleBuilder.build_phase_oracle(violation_states, num_qubits)
|
| 86 |
+
diffuser = OracleBuilder.build_diffuser(num_qubits)
|
| 87 |
+
num_iterations = OracleBuilder.optimal_iterations(
|
| 88 |
+
total_states, n_violations
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
grover_circuit = OracleBuilder.build_grover_circuit(
|
| 92 |
+
oracle, diffuser, num_qubits, num_iterations
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Execute
|
| 96 |
+
start = time.perf_counter()
|
| 97 |
+
job = self.backend.run(grover_circuit, shots=shots)
|
| 98 |
+
result = job.result()
|
| 99 |
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
| 100 |
+
|
| 101 |
+
counts = result.get_counts(grover_circuit)
|
| 102 |
+
|
| 103 |
+
# Sort by measurement count (descending)
|
| 104 |
+
sorted_counts = sorted(
|
| 105 |
+
counts.items(), key=lambda x: x[1], reverse=True
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Identify which top-measured states are actual violations
|
| 109 |
+
violation_set = set(violation_states)
|
| 110 |
+
found: List[int] = []
|
| 111 |
+
for bitstring, _ in sorted_counts:
|
| 112 |
+
state_idx = int(bitstring, 2)
|
| 113 |
+
if state_idx in violation_set:
|
| 114 |
+
found.append(state_idx)
|
| 115 |
+
|
| 116 |
+
return SearchResult(
|
| 117 |
+
measured_states=counts,
|
| 118 |
+
top_states=sorted_counts,
|
| 119 |
+
num_shots=shots,
|
| 120 |
+
num_iterations=num_iterations,
|
| 121 |
+
circuit_depth=grover_circuit.depth(),
|
| 122 |
+
circuit_gate_count=grover_circuit.size(),
|
| 123 |
+
execution_time_ms=elapsed_ms,
|
| 124 |
+
violation_states_found=found,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def classical_search(
|
| 128 |
+
self,
|
| 129 |
+
evaluate_fn: Callable[[int], bool],
|
| 130 |
+
num_qubits: int,
|
| 131 |
+
) -> Tuple[List[int], float]:
|
| 132 |
+
"""Classical brute-force search (for benchmarking).
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
evaluate_fn: Returns True if a state index is a violation.
|
| 136 |
+
num_qubits: Number of boolean variables.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Tuple of (violation_indices, time_in_ms).
|
| 140 |
+
"""
|
| 141 |
+
total = 2**num_qubits
|
| 142 |
+
violations: List[int] = []
|
| 143 |
+
|
| 144 |
+
start = time.perf_counter()
|
| 145 |
+
for state in range(total):
|
| 146 |
+
if evaluate_fn(state):
|
| 147 |
+
violations.append(state)
|
| 148 |
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
| 149 |
+
|
| 150 |
+
return violations, elapsed_ms
|
| 151 |
+
|
| 152 |
+
def benchmark(
|
| 153 |
+
self,
|
| 154 |
+
violation_states: List[int],
|
| 155 |
+
num_qubits: int,
|
| 156 |
+
shots: int = 2048,
|
| 157 |
+
) -> Dict:
|
| 158 |
+
"""Benchmark quantum vs classical search.
|
| 159 |
+
|
| 160 |
+
Returns a dict with timing, speedup, and accuracy metrics.
|
| 161 |
+
"""
|
| 162 |
+
# Quantum search
|
| 163 |
+
q_result = self.search(violation_states, num_qubits, shots=shots)
|
| 164 |
+
|
| 165 |
+
# Classical search
|
| 166 |
+
violation_set = set(violation_states)
|
| 167 |
+
_, classical_ms = self.classical_search(
|
| 168 |
+
lambda s: s in violation_set, num_qubits
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# Theoretical complexity analysis
|
| 172 |
+
total_states = 2**num_qubits
|
| 173 |
+
n_violations = len(violation_states)
|
| 174 |
+
theoretical_classical = total_states
|
| 175 |
+
theoretical_quantum = (
|
| 176 |
+
math.pi / 4 * math.sqrt(total_states / max(n_violations, 1))
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
"num_qubits": num_qubits,
|
| 181 |
+
"total_states": total_states,
|
| 182 |
+
"num_violations": n_violations,
|
| 183 |
+
"quantum_time_ms": q_result.execution_time_ms,
|
| 184 |
+
"classical_time_ms": classical_ms,
|
| 185 |
+
"measured_speedup": (
|
| 186 |
+
classical_ms / max(q_result.execution_time_ms, 0.001)
|
| 187 |
+
),
|
| 188 |
+
"theoretical_speedup": (
|
| 189 |
+
theoretical_classical / max(theoretical_quantum, 1)
|
| 190 |
+
),
|
| 191 |
+
"grover_iterations": q_result.num_iterations,
|
| 192 |
+
"circuit_depth": q_result.circuit_depth,
|
| 193 |
+
"violations_found": len(q_result.violation_states_found),
|
| 194 |
+
"violations_total": n_violations,
|
| 195 |
+
"detection_rate": (
|
| 196 |
+
len(q_result.violation_states_found) / max(n_violations, 1)
|
| 197 |
+
),
|
| 198 |
+
}
|
qsvaps/llm_interface.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM integration for plan generation and repair.
|
| 3 |
+
|
| 4 |
+
Supports OpenAI-compatible APIs and includes a deterministic mock mode
|
| 5 |
+
that works without API keys for demos and testing.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
|
| 11 |
+
from .models import (
|
| 12 |
+
Plan,
|
| 13 |
+
PlanAction,
|
| 14 |
+
PlanConstraint,
|
| 15 |
+
ConstraintType,
|
| 16 |
+
ResourceConstraint,
|
| 17 |
+
FailureWitness,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class LLMInterface:
|
| 22 |
+
"""Handles LLM communication for plan generation and repair.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
api_key: OpenAI API key (or compatible).
|
| 26 |
+
model: Model name (e.g. "gpt-4", "gpt-4o").
|
| 27 |
+
mock: If True, use deterministic mock responses (no API needed).
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
api_key: Optional[str] = None,
|
| 33 |
+
model: str = "gpt-4",
|
| 34 |
+
mock: bool = False,
|
| 35 |
+
):
|
| 36 |
+
self.api_key = api_key
|
| 37 |
+
self.model = model
|
| 38 |
+
self.mock = mock
|
| 39 |
+
self._client = None
|
| 40 |
+
|
| 41 |
+
if api_key and not mock:
|
| 42 |
+
try:
|
| 43 |
+
from openai import OpenAI
|
| 44 |
+
self._client = OpenAI(api_key=api_key)
|
| 45 |
+
except ImportError:
|
| 46 |
+
print(
|
| 47 |
+
"⚠️ openai package not installed. "
|
| 48 |
+
"Falling back to mock mode."
|
| 49 |
+
)
|
| 50 |
+
self.mock = True
|
| 51 |
+
|
| 52 |
+
# ─── Public API ───────────────────────────────────────────────────────
|
| 53 |
+
|
| 54 |
+
def generate_plan(self, task_description: str) -> Optional[Plan]:
|
| 55 |
+
"""Generate a structured plan from a natural-language task."""
|
| 56 |
+
if self.mock:
|
| 57 |
+
return self._mock_generate(task_description)
|
| 58 |
+
|
| 59 |
+
prompt = self._generation_prompt(task_description)
|
| 60 |
+
response = self._call_llm(prompt)
|
| 61 |
+
return self._parse_plan_json(response) if response else None
|
| 62 |
+
|
| 63 |
+
def repair_plan(
|
| 64 |
+
self, plan: Plan, witnesses: List[FailureWitness]
|
| 65 |
+
) -> Optional[Plan]:
|
| 66 |
+
"""Repair a plan based on failure witnesses from verification."""
|
| 67 |
+
if self.mock:
|
| 68 |
+
return self._mock_repair(plan, witnesses)
|
| 69 |
+
|
| 70 |
+
prompt = self._repair_prompt(plan, witnesses)
|
| 71 |
+
response = self._call_llm(prompt)
|
| 72 |
+
return self._parse_plan_json(response) if response else None
|
| 73 |
+
|
| 74 |
+
# ─── LLM Communication ────────────────────────────────────────────────
|
| 75 |
+
|
| 76 |
+
def _call_llm(self, prompt: str) -> Optional[str]:
|
| 77 |
+
"""Make an API call to the LLM."""
|
| 78 |
+
if not self._client:
|
| 79 |
+
return None
|
| 80 |
+
try:
|
| 81 |
+
resp = self._client.chat.completions.create(
|
| 82 |
+
model=self.model,
|
| 83 |
+
messages=[
|
| 84 |
+
{
|
| 85 |
+
"role": "system",
|
| 86 |
+
"content": (
|
| 87 |
+
"You are an expert AI agent planner. "
|
| 88 |
+
"Respond with valid JSON only."
|
| 89 |
+
),
|
| 90 |
+
},
|
| 91 |
+
{"role": "user", "content": prompt},
|
| 92 |
+
],
|
| 93 |
+
temperature=0.3,
|
| 94 |
+
)
|
| 95 |
+
return resp.choices[0].message.content
|
| 96 |
+
except Exception as e:
|
| 97 |
+
print(f"LLM API error: {e}")
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
# ─── Prompt Construction ──────────────────────────────────────────────
|
| 101 |
+
|
| 102 |
+
@staticmethod
|
| 103 |
+
def _generation_prompt(task_description: str) -> str:
|
| 104 |
+
return (
|
| 105 |
+
"Generate a detailed step-by-step plan for the following task.\n\n"
|
| 106 |
+
f"Task: {task_description}\n\n"
|
| 107 |
+
"Return ONLY valid JSON with this structure:\n"
|
| 108 |
+
'{\n'
|
| 109 |
+
' "name": "plan name",\n'
|
| 110 |
+
' "actions": [\n'
|
| 111 |
+
' {\n'
|
| 112 |
+
' "name": "action_name",\n'
|
| 113 |
+
' "description": "what this action does",\n'
|
| 114 |
+
' "resources": ["resource1"],\n'
|
| 115 |
+
' "can_fail": true,\n'
|
| 116 |
+
' "fallback": null\n'
|
| 117 |
+
' }\n'
|
| 118 |
+
' ],\n'
|
| 119 |
+
' "dependencies": [["prerequisite", "dependent"]],\n'
|
| 120 |
+
' "resource_constraints": [\n'
|
| 121 |
+
' {"resource_name": "res", "max_concurrent": 1}\n'
|
| 122 |
+
' ]\n'
|
| 123 |
+
"}\n"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
@staticmethod
|
| 127 |
+
def _repair_prompt(plan: Plan, witnesses: List[FailureWitness]) -> str:
|
| 128 |
+
plan_lines = [f"Plan: {plan.name}"]
|
| 129 |
+
for i, a in enumerate(plan.actions):
|
| 130 |
+
plan_lines.append(f" {i+1}. {a.name}: {a.description}")
|
| 131 |
+
if a.resources:
|
| 132 |
+
plan_lines.append(
|
| 133 |
+
f" Resources: {', '.join(a.resources)}"
|
| 134 |
+
)
|
| 135 |
+
if plan.dependencies:
|
| 136 |
+
plan_lines.append(" Dependencies:")
|
| 137 |
+
for pre, dep in plan.dependencies:
|
| 138 |
+
plan_lines.append(f" {pre} → {dep}")
|
| 139 |
+
plan_text = "\n".join(plan_lines)
|
| 140 |
+
|
| 141 |
+
witness_texts = []
|
| 142 |
+
for i, w in enumerate(witnesses):
|
| 143 |
+
witness_texts.append(f"Failure #{i+1}:\n{w.explanation}")
|
| 144 |
+
witness_text = "\n\n".join(witness_texts)
|
| 145 |
+
|
| 146 |
+
return (
|
| 147 |
+
"The following agent plan has constraint violations.\n\n"
|
| 148 |
+
f"CURRENT PLAN:\n{plan_text}\n\n"
|
| 149 |
+
f"FAILURE SCENARIOS:\n{witness_text}\n\n"
|
| 150 |
+
"Repair the plan by adding fallback actions, fixing "
|
| 151 |
+
"dependencies, or restructuring. Return ONLY valid JSON "
|
| 152 |
+
"with the same schema."
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# ─── JSON Parsing ─────────────────────────────────────────────────────
|
| 156 |
+
|
| 157 |
+
@staticmethod
|
| 158 |
+
def _parse_plan_json(response: str) -> Optional[Plan]:
|
| 159 |
+
"""Extract and parse a Plan from an LLM response."""
|
| 160 |
+
try:
|
| 161 |
+
start = response.find("{")
|
| 162 |
+
end = response.rfind("}") + 1
|
| 163 |
+
if start == -1 or end == 0:
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
data = json.loads(response[start:end])
|
| 167 |
+
|
| 168 |
+
actions = [
|
| 169 |
+
PlanAction(
|
| 170 |
+
name=a["name"],
|
| 171 |
+
description=a.get("description", ""),
|
| 172 |
+
preconditions=a.get("preconditions", []),
|
| 173 |
+
effects=a.get("effects", {}),
|
| 174 |
+
resources=a.get("resources", []),
|
| 175 |
+
can_fail=a.get("can_fail", True),
|
| 176 |
+
fallback=a.get("fallback"),
|
| 177 |
+
)
|
| 178 |
+
for a in data.get("actions", [])
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
rcs = [
|
| 182 |
+
ResourceConstraint(
|
| 183 |
+
resource_name=rc["resource_name"],
|
| 184 |
+
max_concurrent=rc.get("max_concurrent", 1),
|
| 185 |
+
)
|
| 186 |
+
for rc in data.get("resource_constraints", [])
|
| 187 |
+
]
|
| 188 |
+
|
| 189 |
+
return Plan(
|
| 190 |
+
name=data.get("name", "Repaired Plan"),
|
| 191 |
+
actions=actions,
|
| 192 |
+
dependencies=[
|
| 193 |
+
tuple(d) for d in data.get("dependencies", [])
|
| 194 |
+
],
|
| 195 |
+
resource_constraints=rcs,
|
| 196 |
+
)
|
| 197 |
+
except (json.JSONDecodeError, KeyError, TypeError):
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
+
# ─── Mock Implementations ─────────────────────────────────────────────
|
| 201 |
+
|
| 202 |
+
@staticmethod
|
| 203 |
+
def _mock_generate(task_description: str) -> Plan:
|
| 204 |
+
"""Deterministic mock plan for demos."""
|
| 205 |
+
return Plan(
|
| 206 |
+
name="Mock Plan",
|
| 207 |
+
actions=[
|
| 208 |
+
PlanAction(
|
| 209 |
+
"step_1", "Initialize system", can_fail=False
|
| 210 |
+
),
|
| 211 |
+
PlanAction(
|
| 212 |
+
"step_2", "Process data", resources=["gpu"]
|
| 213 |
+
),
|
| 214 |
+
PlanAction("step_3", "Validate results"),
|
| 215 |
+
],
|
| 216 |
+
dependencies=[("step_1", "step_2"), ("step_2", "step_3")],
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
@staticmethod
|
| 220 |
+
def _mock_repair(
|
| 221 |
+
plan: Plan, witnesses: List[FailureWitness]
|
| 222 |
+
) -> Optional[Plan]:
|
| 223 |
+
"""Deterministic mock repair: add fallbacks for failed actions."""
|
| 224 |
+
new_actions = list(plan.actions)
|
| 225 |
+
new_deps = list(plan.dependencies)
|
| 226 |
+
|
| 227 |
+
# Find which actions fail in the witness scenarios
|
| 228 |
+
failed_actions = set()
|
| 229 |
+
for witness in witnesses:
|
| 230 |
+
for var_name, val in witness.assignment.items():
|
| 231 |
+
if var_name.startswith("s_") and not val:
|
| 232 |
+
action_name = var_name[2:]
|
| 233 |
+
action = plan.get_action(action_name)
|
| 234 |
+
if action and action.can_fail:
|
| 235 |
+
failed_actions.add(action_name)
|
| 236 |
+
|
| 237 |
+
# Add a fallback for each failing action
|
| 238 |
+
for action_name in sorted(failed_actions):
|
| 239 |
+
original = plan.get_action(action_name)
|
| 240 |
+
if original and not original.fallback:
|
| 241 |
+
fb_name = f"{action_name}_fallback"
|
| 242 |
+
fallback = PlanAction(
|
| 243 |
+
name=fb_name,
|
| 244 |
+
description=f"Fallback for {original.description}",
|
| 245 |
+
preconditions=original.preconditions,
|
| 246 |
+
effects=original.effects,
|
| 247 |
+
resources=original.resources,
|
| 248 |
+
can_fail=False,
|
| 249 |
+
)
|
| 250 |
+
new_actions.append(fallback)
|
| 251 |
+
original.fallback = fb_name
|
| 252 |
+
|
| 253 |
+
# Carry forward dependencies
|
| 254 |
+
for pre, dep in plan.dependencies:
|
| 255 |
+
if dep == action_name:
|
| 256 |
+
new_deps.append((pre, fb_name))
|
| 257 |
+
|
| 258 |
+
return Plan(
|
| 259 |
+
name=f"{plan.name} (repaired)",
|
| 260 |
+
actions=new_actions,
|
| 261 |
+
dependencies=new_deps,
|
| 262 |
+
resource_constraints=plan.resource_constraints,
|
| 263 |
+
custom_constraints=plan.custom_constraints,
|
| 264 |
+
)
|
qsvaps/models.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data models for QSVAPS.
|
| 3 |
+
|
| 4 |
+
Defines the core data structures: Plan, PlanAction, Constraint,
|
| 5 |
+
FailureWitness, and VerificationResult.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from dataclasses import dataclass, field
|
| 9 |
+
from enum import Enum
|
| 10 |
+
from typing import Dict, List, Optional, Tuple
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ─── Enums ────────────────────────────────────────────────────────────────────
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ConstraintType(Enum):
|
| 17 |
+
"""Types of constraints that can be extracted from a plan."""
|
| 18 |
+
DEPENDENCY = "dependency"
|
| 19 |
+
RESOURCE = "resource"
|
| 20 |
+
COMPLETION = "completion"
|
| 21 |
+
ORDERING = "ordering"
|
| 22 |
+
MUTUAL_EXCLUSION = "mutual_exclusion"
|
| 23 |
+
FALLBACK = "fallback"
|
| 24 |
+
CUSTOM = "custom"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ─── Plan Components ─────────────────────────────────────────────────────────
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class PlanAction:
|
| 32 |
+
"""A single action/step in an agent's plan.
|
| 33 |
+
|
| 34 |
+
Attributes:
|
| 35 |
+
name: Unique identifier for this action.
|
| 36 |
+
description: Human-readable description of what this action does.
|
| 37 |
+
preconditions: List of variable names that must be True before this
|
| 38 |
+
action can execute.
|
| 39 |
+
effects: Dict mapping variable names to the boolean values they take
|
| 40 |
+
after this action completes.
|
| 41 |
+
resources: List of shared resource names this action consumes.
|
| 42 |
+
can_fail: Whether this action is allowed to fail. If False, a
|
| 43 |
+
completion constraint is generated.
|
| 44 |
+
fallback: Name of a fallback action to use if this one fails.
|
| 45 |
+
"""
|
| 46 |
+
name: str
|
| 47 |
+
description: str = ""
|
| 48 |
+
preconditions: List[str] = field(default_factory=list)
|
| 49 |
+
effects: Dict[str, bool] = field(default_factory=dict)
|
| 50 |
+
resources: List[str] = field(default_factory=list)
|
| 51 |
+
can_fail: bool = True
|
| 52 |
+
fallback: Optional[str] = None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class ResourceConstraint:
|
| 57 |
+
"""Constraint on a shared resource.
|
| 58 |
+
|
| 59 |
+
Attributes:
|
| 60 |
+
resource_name: Identifier of the shared resource.
|
| 61 |
+
max_concurrent: Maximum number of actions that can use this
|
| 62 |
+
resource simultaneously.
|
| 63 |
+
"""
|
| 64 |
+
resource_name: str
|
| 65 |
+
max_concurrent: int = 1
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@dataclass
|
| 69 |
+
class PlanConstraint:
|
| 70 |
+
"""A boolean constraint on plan execution.
|
| 71 |
+
|
| 72 |
+
Attributes:
|
| 73 |
+
expression: Boolean expression string using variables like x0, x1, ...
|
| 74 |
+
Supports Python boolean operators: and, or, not.
|
| 75 |
+
description: Human-readable explanation of what this constraint means.
|
| 76 |
+
constraint_type: Category of constraint.
|
| 77 |
+
variables_involved: List of variable names referenced by this
|
| 78 |
+
constraint.
|
| 79 |
+
"""
|
| 80 |
+
expression: str
|
| 81 |
+
description: str
|
| 82 |
+
constraint_type: ConstraintType
|
| 83 |
+
variables_involved: List[str] = field(default_factory=list)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class Plan:
|
| 88 |
+
"""A multi-step agent plan with actions, dependencies, and constraints.
|
| 89 |
+
|
| 90 |
+
Attributes:
|
| 91 |
+
name: Human-readable name for this plan.
|
| 92 |
+
actions: Ordered list of PlanAction objects.
|
| 93 |
+
dependencies: List of (prerequisite, dependent) action name pairs.
|
| 94 |
+
resource_constraints: List of ResourceConstraint objects.
|
| 95 |
+
custom_constraints: Additional PlanConstraint objects defined by the user.
|
| 96 |
+
"""
|
| 97 |
+
name: str
|
| 98 |
+
actions: List[PlanAction]
|
| 99 |
+
dependencies: List[Tuple[str, str]] = field(default_factory=list)
|
| 100 |
+
resource_constraints: List[ResourceConstraint] = field(default_factory=list)
|
| 101 |
+
custom_constraints: List[PlanConstraint] = field(default_factory=list)
|
| 102 |
+
|
| 103 |
+
def get_action(self, name: str) -> Optional[PlanAction]:
|
| 104 |
+
"""Look up an action by name."""
|
| 105 |
+
for action in self.actions:
|
| 106 |
+
if action.name == name:
|
| 107 |
+
return action
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
def get_action_index(self, name: str) -> int:
|
| 111 |
+
"""Get the index of an action by name. Returns -1 if not found."""
|
| 112 |
+
for i, action in enumerate(self.actions):
|
| 113 |
+
if action.name == name:
|
| 114 |
+
return i
|
| 115 |
+
return -1
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def action_names(self) -> List[str]:
|
| 119 |
+
"""List of all action names in order."""
|
| 120 |
+
return [a.name for a in self.actions]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# ─── Variable Mapping ────────────────────────────────────────────────────────
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@dataclass
|
| 127 |
+
class VariableMapping:
|
| 128 |
+
"""Maps plan properties to boolean qubit variables.
|
| 129 |
+
|
| 130 |
+
Each boolean variable corresponds to one qubit in the quantum circuit.
|
| 131 |
+
|
| 132 |
+
Attributes:
|
| 133 |
+
variables: Mapping from variable name to qubit index.
|
| 134 |
+
descriptions: Mapping from variable name to human-readable description.
|
| 135 |
+
"""
|
| 136 |
+
variables: Dict[str, int] = field(default_factory=dict)
|
| 137 |
+
descriptions: Dict[str, str] = field(default_factory=dict)
|
| 138 |
+
|
| 139 |
+
@property
|
| 140 |
+
def num_variables(self) -> int:
|
| 141 |
+
"""Total number of boolean variables (= number of qubits needed)."""
|
| 142 |
+
return len(self.variables)
|
| 143 |
+
|
| 144 |
+
def get_var_name(self, index: int) -> str:
|
| 145 |
+
"""Get the variable name for a given qubit index."""
|
| 146 |
+
for name, idx in self.variables.items():
|
| 147 |
+
if idx == index:
|
| 148 |
+
return name
|
| 149 |
+
return f"x{index}"
|
| 150 |
+
|
| 151 |
+
def get_description(self, index: int) -> str:
|
| 152 |
+
"""Get the human-readable description for a given qubit index."""
|
| 153 |
+
name = self.get_var_name(index)
|
| 154 |
+
return self.descriptions.get(name, name)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# ─── Verification Results ────────────────────────────────────────────────────
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@dataclass
|
| 161 |
+
class FailureWitness:
|
| 162 |
+
"""A specific failure scenario discovered by quantum verification.
|
| 163 |
+
|
| 164 |
+
Attributes:
|
| 165 |
+
assignment: Mapping from variable names to their boolean values in
|
| 166 |
+
this failure scenario.
|
| 167 |
+
violated_constraints: List of constraints that are violated.
|
| 168 |
+
bitstring: Raw qubit measurement bitstring.
|
| 169 |
+
measurement_count: Number of times this state was measured (higher
|
| 170 |
+
counts indicate Grover amplification targeted this state).
|
| 171 |
+
explanation: Human-readable explanation of why this scenario fails.
|
| 172 |
+
"""
|
| 173 |
+
assignment: Dict[str, bool]
|
| 174 |
+
violated_constraints: List[PlanConstraint]
|
| 175 |
+
bitstring: str
|
| 176 |
+
measurement_count: int = 0
|
| 177 |
+
explanation: str = ""
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@dataclass
|
| 181 |
+
class VerificationResult:
|
| 182 |
+
"""Complete result of quantum plan verification.
|
| 183 |
+
|
| 184 |
+
Attributes:
|
| 185 |
+
plan: The plan that was verified.
|
| 186 |
+
is_safe: True if no constraint violations were found.
|
| 187 |
+
witnesses: List of FailureWitness objects (most significant first).
|
| 188 |
+
num_variables: Number of boolean variables / qubits.
|
| 189 |
+
num_violations: Total number of violating states in the state space.
|
| 190 |
+
total_states: Total size of the state space (2^num_variables).
|
| 191 |
+
grover_iterations: Number of Grover iterations used.
|
| 192 |
+
quantum_time_ms: Wall-clock time for quantum circuit execution.
|
| 193 |
+
classical_time_ms: Wall-clock time for classical brute-force search.
|
| 194 |
+
circuit_depth: Depth of the quantum circuit.
|
| 195 |
+
circuit_gate_count: Total number of gates in the quantum circuit.
|
| 196 |
+
speedup_factor: Theoretical quantum speedup factor.
|
| 197 |
+
"""
|
| 198 |
+
plan: Plan
|
| 199 |
+
is_safe: bool
|
| 200 |
+
witnesses: List[FailureWitness]
|
| 201 |
+
num_variables: int
|
| 202 |
+
num_violations: int
|
| 203 |
+
total_states: int
|
| 204 |
+
grover_iterations: int
|
| 205 |
+
quantum_time_ms: float = 0.0
|
| 206 |
+
classical_time_ms: float = 0.0
|
| 207 |
+
circuit_depth: int = 0
|
| 208 |
+
circuit_gate_count: int = 0
|
| 209 |
+
speedup_factor: float = 0.0
|
qsvaps/oracle_builder.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quantum oracle construction for QSVAPS.
|
| 3 |
+
|
| 4 |
+
Builds Qiskit QuantumCircuit objects that encode plan constraint violations
|
| 5 |
+
as phase-flip oracles for use in Grover's algorithm.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import math
|
| 9 |
+
from typing import List
|
| 10 |
+
|
| 11 |
+
from qiskit import QuantumCircuit
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class OracleBuilder:
|
| 15 |
+
"""Builds quantum oracle and diffuser circuits for Grover's search.
|
| 16 |
+
|
| 17 |
+
The oracle marks constraint-violating states with a phase flip:
|
| 18 |
+
|v⟩ → -|v⟩ for violation states
|
| 19 |
+
|s⟩ → |s⟩ for safe states
|
| 20 |
+
|
| 21 |
+
Combined with the Grover diffuser, this amplifies the probability
|
| 22 |
+
of measuring violation states.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
def build_phase_oracle(
|
| 27 |
+
violation_states: List[int], num_qubits: int
|
| 28 |
+
) -> QuantumCircuit:
|
| 29 |
+
"""Build a phase oracle that flips the phase of violation states.
|
| 30 |
+
|
| 31 |
+
For each violation state |v⟩, applies a multi-controlled Z that
|
| 32 |
+
maps |v⟩ → -|v⟩. Uses X gates to convert each target state to
|
| 33 |
+
the all-ones state before applying the controlled-Z.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
violation_states: List of state indices (integers) to mark.
|
| 37 |
+
num_qubits: Number of qubits in the circuit.
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
A QuantumCircuit acting as a phase oracle.
|
| 41 |
+
"""
|
| 42 |
+
oracle = QuantumCircuit(num_qubits, name="Violation_Oracle")
|
| 43 |
+
|
| 44 |
+
if not violation_states:
|
| 45 |
+
return oracle
|
| 46 |
+
|
| 47 |
+
for state in violation_states:
|
| 48 |
+
# Convert state to bit pattern (qubit 0 = LSB)
|
| 49 |
+
bits = format(state, f"0{num_qubits}b")[::-1]
|
| 50 |
+
|
| 51 |
+
# Flip qubits that should be |0⟩ in this state
|
| 52 |
+
for i, bit in enumerate(bits):
|
| 53 |
+
if bit == "0":
|
| 54 |
+
oracle.x(i)
|
| 55 |
+
|
| 56 |
+
# Multi-controlled Z gate: applies -1 phase to |11...1⟩
|
| 57 |
+
if num_qubits == 1:
|
| 58 |
+
oracle.z(0)
|
| 59 |
+
else:
|
| 60 |
+
# CZ decomposition: H on target → MCX → H on target
|
| 61 |
+
oracle.h(num_qubits - 1)
|
| 62 |
+
oracle.mcx(list(range(num_qubits - 1)), num_qubits - 1)
|
| 63 |
+
oracle.h(num_qubits - 1)
|
| 64 |
+
|
| 65 |
+
# Undo the X flips
|
| 66 |
+
for i, bit in enumerate(bits):
|
| 67 |
+
if bit == "0":
|
| 68 |
+
oracle.x(i)
|
| 69 |
+
|
| 70 |
+
return oracle
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def build_diffuser(num_qubits: int) -> QuantumCircuit:
|
| 74 |
+
"""Build the Grover diffusion operator.
|
| 75 |
+
|
| 76 |
+
Implements D = 2|s⟩⟨s| - I where |s⟩ = H^⊗n|0⟩ is the
|
| 77 |
+
uniform superposition state.
|
| 78 |
+
|
| 79 |
+
Circuit: H^⊗n → X^⊗n → MCZ → X^⊗n → H^⊗n
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
num_qubits: Number of qubits.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
A QuantumCircuit implementing the diffuser.
|
| 86 |
+
"""
|
| 87 |
+
diffuser = QuantumCircuit(num_qubits, name="Diffuser")
|
| 88 |
+
|
| 89 |
+
# Transform to computational basis
|
| 90 |
+
diffuser.h(range(num_qubits))
|
| 91 |
+
diffuser.x(range(num_qubits))
|
| 92 |
+
|
| 93 |
+
# Multi-controlled Z on |11...1⟩
|
| 94 |
+
if num_qubits == 1:
|
| 95 |
+
diffuser.z(0)
|
| 96 |
+
else:
|
| 97 |
+
diffuser.h(num_qubits - 1)
|
| 98 |
+
diffuser.mcx(list(range(num_qubits - 1)), num_qubits - 1)
|
| 99 |
+
diffuser.h(num_qubits - 1)
|
| 100 |
+
|
| 101 |
+
# Transform back
|
| 102 |
+
diffuser.x(range(num_qubits))
|
| 103 |
+
diffuser.h(range(num_qubits))
|
| 104 |
+
|
| 105 |
+
return diffuser
|
| 106 |
+
|
| 107 |
+
@staticmethod
|
| 108 |
+
def optimal_iterations(total_states: int, num_violations: int) -> int:
|
| 109 |
+
"""Calculate optimal number of Grover iterations.
|
| 110 |
+
|
| 111 |
+
The optimal count is:
|
| 112 |
+
k = round(π/4 × √(N/M))
|
| 113 |
+
|
| 114 |
+
where N = total states, M = number of marked (violation) states.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
total_states: Total number of states in the search space (2^n).
|
| 118 |
+
num_violations: Number of states the oracle marks.
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
Optimal number of Grover iterations (minimum 1).
|
| 122 |
+
"""
|
| 123 |
+
if num_violations == 0 or num_violations >= total_states:
|
| 124 |
+
return 0
|
| 125 |
+
|
| 126 |
+
ratio = total_states / num_violations
|
| 127 |
+
k = round(math.pi / 4 * math.sqrt(ratio))
|
| 128 |
+
return max(1, k)
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def build_grover_circuit(
|
| 132 |
+
oracle: QuantumCircuit,
|
| 133 |
+
diffuser: QuantumCircuit,
|
| 134 |
+
num_qubits: int,
|
| 135 |
+
num_iterations: int,
|
| 136 |
+
) -> QuantumCircuit:
|
| 137 |
+
"""Assemble the complete Grover search circuit.
|
| 138 |
+
|
| 139 |
+
Structure: H^⊗n → (Oracle · Diffuser)^k → Measure
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
oracle: The phase oracle circuit.
|
| 143 |
+
diffuser: The diffusion operator circuit.
|
| 144 |
+
num_qubits: Number of qubits.
|
| 145 |
+
num_iterations: Number of Grover iterations to apply.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
A QuantumCircuit with measurement operations.
|
| 149 |
+
"""
|
| 150 |
+
qc = QuantumCircuit(num_qubits, num_qubits, name="QSVAPS_Grover")
|
| 151 |
+
|
| 152 |
+
# Initialize uniform superposition
|
| 153 |
+
qc.h(range(num_qubits))
|
| 154 |
+
qc.barrier()
|
| 155 |
+
|
| 156 |
+
# Grover iterations
|
| 157 |
+
for i in range(num_iterations):
|
| 158 |
+
qc.compose(oracle, inplace=True)
|
| 159 |
+
qc.compose(diffuser, inplace=True)
|
| 160 |
+
if i < num_iterations - 1:
|
| 161 |
+
qc.barrier()
|
| 162 |
+
|
| 163 |
+
qc.barrier()
|
| 164 |
+
|
| 165 |
+
# Measure all qubits
|
| 166 |
+
qc.measure(range(num_qubits), range(num_qubits))
|
| 167 |
+
|
| 168 |
+
return qc
|
qsvaps/verifier.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main verification engine — orchestrates the QSVAPS pipeline.
|
| 3 |
+
|
| 4 |
+
Ties together constraint extraction, oracle construction, Grover search,
|
| 5 |
+
witness decoding, and (optionally) LLM-based plan repair.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
from .models import (
|
| 11 |
+
Plan,
|
| 12 |
+
PlanConstraint,
|
| 13 |
+
FailureWitness,
|
| 14 |
+
VerificationResult,
|
| 15 |
+
VariableMapping,
|
| 16 |
+
)
|
| 17 |
+
from .constraint_engine import ConstraintEngine
|
| 18 |
+
from .grover_search import GroverSearchEngine
|
| 19 |
+
from .llm_interface import LLMInterface
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PlanVerifier:
|
| 23 |
+
"""Quantum-powered plan verification engine.
|
| 24 |
+
|
| 25 |
+
Pipeline:
|
| 26 |
+
1. Extract boolean constraints from the plan
|
| 27 |
+
2. Identify violation states (classical enumeration for oracle)
|
| 28 |
+
3. Run Grover's algorithm to efficiently find violations
|
| 29 |
+
4. Decode violations into human-readable failure witnesses
|
| 30 |
+
5. (Optional) Feed witnesses to LLM for iterative plan repair
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
llm: Optional LLMInterface for plan repair.
|
| 34 |
+
max_repair_iterations: Max repair rounds before giving up.
|
| 35 |
+
shots: Number of quantum measurement shots per Grover run.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
llm: Optional[LLMInterface] = None,
|
| 41 |
+
max_repair_iterations: int = 3,
|
| 42 |
+
shots: int = 2048,
|
| 43 |
+
):
|
| 44 |
+
self.llm = llm
|
| 45 |
+
self.max_repair_iterations = max_repair_iterations
|
| 46 |
+
self.shots = shots
|
| 47 |
+
self.search_engine = GroverSearchEngine()
|
| 48 |
+
|
| 49 |
+
# ─── Core Verification ────────────────────────────────────────────────
|
| 50 |
+
|
| 51 |
+
def verify(self, plan: Plan, verbose: bool = False) -> VerificationResult:
|
| 52 |
+
"""Verify a plan using quantum search for constraint violations.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
plan: The Plan to verify.
|
| 56 |
+
verbose: If True, print step-by-step progress.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
VerificationResult with safety status and failure witnesses.
|
| 60 |
+
"""
|
| 61 |
+
# Step 1: Extract constraints
|
| 62 |
+
engine = ConstraintEngine(plan)
|
| 63 |
+
constraints = engine.extract_constraints()
|
| 64 |
+
|
| 65 |
+
if verbose:
|
| 66 |
+
self._print_header(plan, engine, constraints)
|
| 67 |
+
|
| 68 |
+
# Step 2: Classical enumeration of violations (needed for oracle)
|
| 69 |
+
violations = engine.find_all_violations(constraints)
|
| 70 |
+
|
| 71 |
+
if verbose:
|
| 72 |
+
total = 2**engine.var_mapping.num_variables
|
| 73 |
+
print(f"\n⚠️ Found {len(violations)} violation states "
|
| 74 |
+
f"out of {total:,} total")
|
| 75 |
+
if total > 0:
|
| 76 |
+
print(f" Violation rate: "
|
| 77 |
+
f"{len(violations) / total * 100:.1f}%")
|
| 78 |
+
|
| 79 |
+
# Step 3: Quantum search
|
| 80 |
+
search_result = None
|
| 81 |
+
benchmark: Dict = {"classical_time_ms": 0, "theoretical_speedup": 1}
|
| 82 |
+
|
| 83 |
+
if violations:
|
| 84 |
+
if verbose:
|
| 85 |
+
print(f"\n🔬 Running Grover's quantum search...")
|
| 86 |
+
|
| 87 |
+
search_result = self.search_engine.search(
|
| 88 |
+
violations, engine.var_mapping.num_variables, shots=self.shots
|
| 89 |
+
)
|
| 90 |
+
benchmark = self.search_engine.benchmark(
|
| 91 |
+
violations, engine.var_mapping.num_variables, shots=self.shots
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
if verbose:
|
| 95 |
+
self._print_quantum_stats(search_result, benchmark, violations)
|
| 96 |
+
|
| 97 |
+
# Step 4: Decode failure witnesses
|
| 98 |
+
witnesses: List[FailureWitness] = []
|
| 99 |
+
if search_result and search_result.violation_states_found:
|
| 100 |
+
for state_idx in search_result.violation_states_found[:5]:
|
| 101 |
+
witness = self._build_witness(state_idx, engine, constraints)
|
| 102 |
+
bs = format(
|
| 103 |
+
state_idx, f"0{engine.var_mapping.num_variables}b"
|
| 104 |
+
)
|
| 105 |
+
witness.measurement_count = (
|
| 106 |
+
search_result.measured_states.get(bs, 0)
|
| 107 |
+
)
|
| 108 |
+
witnesses.append(witness)
|
| 109 |
+
|
| 110 |
+
if verbose:
|
| 111 |
+
self._print_witnesses(witnesses, engine)
|
| 112 |
+
|
| 113 |
+
# Build result
|
| 114 |
+
result = VerificationResult(
|
| 115 |
+
plan=plan,
|
| 116 |
+
is_safe=len(violations) == 0,
|
| 117 |
+
witnesses=witnesses,
|
| 118 |
+
num_variables=engine.var_mapping.num_variables,
|
| 119 |
+
num_violations=len(violations),
|
| 120 |
+
total_states=2**engine.var_mapping.num_variables,
|
| 121 |
+
grover_iterations=(
|
| 122 |
+
search_result.num_iterations if search_result else 0
|
| 123 |
+
),
|
| 124 |
+
quantum_time_ms=(
|
| 125 |
+
search_result.execution_time_ms if search_result else 0
|
| 126 |
+
),
|
| 127 |
+
classical_time_ms=benchmark.get("classical_time_ms", 0),
|
| 128 |
+
circuit_depth=(
|
| 129 |
+
search_result.circuit_depth if search_result else 0
|
| 130 |
+
),
|
| 131 |
+
circuit_gate_count=(
|
| 132 |
+
search_result.circuit_gate_count if search_result else 0
|
| 133 |
+
),
|
| 134 |
+
speedup_factor=benchmark.get("theoretical_speedup", 1),
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
if verbose:
|
| 138 |
+
if result.is_safe:
|
| 139 |
+
print(f"\n✅ Plan '{plan.name}' is SAFE — no violations!")
|
| 140 |
+
else:
|
| 141 |
+
print(
|
| 142 |
+
f"\n❌ Plan '{plan.name}' has "
|
| 143 |
+
f"{result.num_violations} violation states"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
return result
|
| 147 |
+
|
| 148 |
+
# ─── Verify + Repair Loop ─────────────────────────────────────────────
|
| 149 |
+
|
| 150 |
+
def verify_and_repair(
|
| 151 |
+
self, plan: Plan, verbose: bool = False
|
| 152 |
+
) -> List[VerificationResult]:
|
| 153 |
+
"""Verify a plan and iteratively repair it using an LLM.
|
| 154 |
+
|
| 155 |
+
The loop runs at most max_repair_iterations + 1 times (one initial
|
| 156 |
+
verification plus repair rounds). Stops early if the plan becomes
|
| 157 |
+
safe or the LLM fails to repair it.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
List of VerificationResult from each round.
|
| 161 |
+
"""
|
| 162 |
+
results: List[VerificationResult] = []
|
| 163 |
+
current_plan = plan
|
| 164 |
+
|
| 165 |
+
for iteration in range(self.max_repair_iterations + 1):
|
| 166 |
+
if verbose and iteration > 0:
|
| 167 |
+
print(f"\n{'='*60}")
|
| 168 |
+
print(f" REPAIR ITERATION {iteration}")
|
| 169 |
+
print(f"{'='*60}")
|
| 170 |
+
|
| 171 |
+
result = self.verify(current_plan, verbose=verbose)
|
| 172 |
+
results.append(result)
|
| 173 |
+
|
| 174 |
+
if result.is_safe:
|
| 175 |
+
if verbose:
|
| 176 |
+
print(
|
| 177 |
+
f"\n🎉 Plan verified safe after "
|
| 178 |
+
f"{iteration} repair(s)!"
|
| 179 |
+
)
|
| 180 |
+
break
|
| 181 |
+
|
| 182 |
+
if iteration < self.max_repair_iterations and self.llm:
|
| 183 |
+
if verbose:
|
| 184 |
+
print(f"\n🔧 Requesting LLM repair...")
|
| 185 |
+
|
| 186 |
+
repaired = self.llm.repair_plan(
|
| 187 |
+
current_plan, result.witnesses
|
| 188 |
+
)
|
| 189 |
+
if repaired:
|
| 190 |
+
current_plan = repaired
|
| 191 |
+
if verbose:
|
| 192 |
+
print(
|
| 193 |
+
f" Repaired plan received with "
|
| 194 |
+
f"{len(repaired.actions)} actions"
|
| 195 |
+
)
|
| 196 |
+
else:
|
| 197 |
+
if verbose:
|
| 198 |
+
print(f" ⚠️ LLM could not repair the plan")
|
| 199 |
+
break
|
| 200 |
+
else:
|
| 201 |
+
# No LLM or max iterations reached — cannot repair
|
| 202 |
+
break
|
| 203 |
+
|
| 204 |
+
return results
|
| 205 |
+
|
| 206 |
+
# ─── Witness Construction ─────────────────────────────────────────────
|
| 207 |
+
|
| 208 |
+
def _build_witness(
|
| 209 |
+
self,
|
| 210 |
+
state_idx: int,
|
| 211 |
+
engine: ConstraintEngine,
|
| 212 |
+
constraints: List[PlanConstraint],
|
| 213 |
+
) -> FailureWitness:
|
| 214 |
+
"""Decode a state index into a human-readable failure witness."""
|
| 215 |
+
n = engine.var_mapping.num_variables
|
| 216 |
+
bits = format(state_idx, f"0{n}b")
|
| 217 |
+
|
| 218 |
+
# Map bits to variable names
|
| 219 |
+
assignment: Dict[str, bool] = {}
|
| 220 |
+
for var_name, var_idx in engine.var_mapping.variables.items():
|
| 221 |
+
assignment[var_name] = bits[n - 1 - var_idx] == "1"
|
| 222 |
+
|
| 223 |
+
# Find violated constraints
|
| 224 |
+
_, violated = engine.evaluate_state(state_idx, constraints)
|
| 225 |
+
|
| 226 |
+
# Build explanation
|
| 227 |
+
lines = ["This scenario fails because:"]
|
| 228 |
+
for vc in violated:
|
| 229 |
+
lines.append(f" • {vc.description}")
|
| 230 |
+
explanation = "\n".join(lines)
|
| 231 |
+
|
| 232 |
+
return FailureWitness(
|
| 233 |
+
assignment=assignment,
|
| 234 |
+
violated_constraints=violated,
|
| 235 |
+
bitstring=bits,
|
| 236 |
+
explanation=explanation,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# ─── Verbose Output Helpers ───────────────────────────────────────────
|
| 240 |
+
|
| 241 |
+
@staticmethod
|
| 242 |
+
def _print_header(
|
| 243 |
+
plan: Plan,
|
| 244 |
+
engine: ConstraintEngine,
|
| 245 |
+
constraints: List[PlanConstraint],
|
| 246 |
+
):
|
| 247 |
+
n = engine.var_mapping.num_variables
|
| 248 |
+
print(f"\n{'='*60}")
|
| 249 |
+
print(f" ⚛️ QSVAPS: Verifying plan '{plan.name}'")
|
| 250 |
+
print(f"{'='*60}")
|
| 251 |
+
print(f"\n📋 Plan: {len(plan.actions)} actions, "
|
| 252 |
+
f"{len(constraints)} constraints")
|
| 253 |
+
print(f"🔢 Boolean variables: {n}")
|
| 254 |
+
print(f"🌌 State space: {2**n:,} possible states")
|
| 255 |
+
print(f"\n📌 Variable mapping:")
|
| 256 |
+
for var_name, idx in engine.var_mapping.variables.items():
|
| 257 |
+
desc = engine.var_mapping.descriptions[var_name]
|
| 258 |
+
print(f" x{idx} = {desc}")
|
| 259 |
+
print(f"\n📌 Constraints:")
|
| 260 |
+
for i, c in enumerate(constraints):
|
| 261 |
+
tag = c.constraint_type.value.upper()
|
| 262 |
+
print(f" C{i + 1} [{tag}]: {c.description}")
|
| 263 |
+
print(f" Formula: {c.expression}")
|
| 264 |
+
|
| 265 |
+
@staticmethod
|
| 266 |
+
def _print_quantum_stats(search_result, benchmark, violations):
|
| 267 |
+
print(f" Grover iterations: {search_result.num_iterations}")
|
| 268 |
+
print(f" Circuit depth: {search_result.circuit_depth}")
|
| 269 |
+
print(f" Circuit gates: {search_result.circuit_gate_count}")
|
| 270 |
+
print(f" Quantum time: "
|
| 271 |
+
f"{search_result.execution_time_ms:.2f} ms")
|
| 272 |
+
print(f" Classical time: "
|
| 273 |
+
f"{benchmark['classical_time_ms']:.2f} ms")
|
| 274 |
+
print(f" Theoretical ×: "
|
| 275 |
+
f"{benchmark['theoretical_speedup']:.1f}×")
|
| 276 |
+
print(f" Detection: "
|
| 277 |
+
f"{len(search_result.violation_states_found)}"
|
| 278 |
+
f"/{len(violations)}")
|
| 279 |
+
|
| 280 |
+
@staticmethod
|
| 281 |
+
def _print_witnesses(witnesses, engine):
|
| 282 |
+
print(f"\n🔍 Failure Witnesses (top {len(witnesses)}):")
|
| 283 |
+
for i, w in enumerate(witnesses):
|
| 284 |
+
print(f"\n Witness #{i + 1} "
|
| 285 |
+
f"(measured {w.measurement_count}×):")
|
| 286 |
+
print(f" Bitstring: {w.bitstring}")
|
| 287 |
+
print(f" Scenario:")
|
| 288 |
+
for var_name, val in w.assignment.items():
|
| 289 |
+
desc = engine.var_mapping.descriptions.get(
|
| 290 |
+
var_name, var_name
|
| 291 |
+
)
|
| 292 |
+
icon = "✅" if val else "❌"
|
| 293 |
+
print(f" {icon} {desc}")
|
| 294 |
+
print(f" Violated:")
|
| 295 |
+
for vc in w.violated_constraints:
|
| 296 |
+
print(f" ⛔ {vc.description}")
|
qsvaps/visualization.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Visualization utilities for QSVAPS.
|
| 3 |
+
|
| 4 |
+
Provides formatted text output for plans, verification results,
|
| 5 |
+
and measurement histograms.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
|
| 10 |
+
from .models import Plan, VerificationResult, FailureWitness
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class QSVAPSVisualizer:
|
| 14 |
+
"""Visualization and formatting utilities for QSVAPS output."""
|
| 15 |
+
|
| 16 |
+
@staticmethod
|
| 17 |
+
def draw_plan(plan: Plan) -> str:
|
| 18 |
+
"""Create an ASCII box diagram of a plan."""
|
| 19 |
+
w = 58
|
| 20 |
+
lines = []
|
| 21 |
+
lines.append(f"╔{'═' * w}╗")
|
| 22 |
+
lines.append(f"║ Plan: {plan.name:<{w - 9}}║")
|
| 23 |
+
lines.append(f"╠{'═' * w}╣")
|
| 24 |
+
|
| 25 |
+
for i, action in enumerate(plan.actions):
|
| 26 |
+
symbol = "●" if not action.can_fail else "○"
|
| 27 |
+
label = f"{symbol} Step {i + 1}: {action.name}"
|
| 28 |
+
lines.append(f"║ {label:<{w - 4}}║")
|
| 29 |
+
|
| 30 |
+
if action.description:
|
| 31 |
+
desc = action.description[: w - 10]
|
| 32 |
+
lines.append(f"║ └─ {desc:<{w - 9}}║")
|
| 33 |
+
if action.resources:
|
| 34 |
+
res = ", ".join(action.resources)[: w - 20]
|
| 35 |
+
lines.append(f"║ └─ Resources: {res:<{w - 20}}║")
|
| 36 |
+
if action.fallback:
|
| 37 |
+
fb = action.fallback[: w - 20]
|
| 38 |
+
lines.append(f"║ └─ Fallback: {fb:<{w - 19}}║")
|
| 39 |
+
|
| 40 |
+
if plan.dependencies:
|
| 41 |
+
lines.append(f"╠{'═' * w}╣")
|
| 42 |
+
lines.append(f"║ Dependencies:{' ' * (w - 16)}║")
|
| 43 |
+
for pre, dep in plan.dependencies:
|
| 44 |
+
arrow = f"{pre} → {dep}"[: w - 6]
|
| 45 |
+
lines.append(f"║ {arrow:<{w - 6}}║")
|
| 46 |
+
|
| 47 |
+
if plan.resource_constraints:
|
| 48 |
+
lines.append(f"╠{'═' * w}╣")
|
| 49 |
+
lines.append(f"║ Resource Constraints:{' ' * (w - 24)}║")
|
| 50 |
+
for rc in plan.resource_constraints:
|
| 51 |
+
desc = (
|
| 52 |
+
f"{rc.resource_name} "
|
| 53 |
+
f"(max concurrent: {rc.max_concurrent})"
|
| 54 |
+
)[: w - 6]
|
| 55 |
+
lines.append(f"║ {desc:<{w - 6}}║")
|
| 56 |
+
|
| 57 |
+
lines.append(f"╚{'═' * w}╝")
|
| 58 |
+
return "\n".join(lines)
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def format_result(result: VerificationResult) -> str:
|
| 62 |
+
"""Format a verification result as a readable report."""
|
| 63 |
+
status = "✅ SAFE" if result.is_safe else "❌ UNSAFE"
|
| 64 |
+
w = 60
|
| 65 |
+
|
| 66 |
+
lines = []
|
| 67 |
+
lines.append(f"\n{'━' * w}")
|
| 68 |
+
lines.append(f" ⚛️ QSVAPS Verification Report")
|
| 69 |
+
lines.append(f"{'━' * w}")
|
| 70 |
+
lines.append(f" Plan: {result.plan.name}")
|
| 71 |
+
lines.append(f" Status: {status}")
|
| 72 |
+
lines.append(f"{'─' * w}")
|
| 73 |
+
lines.append(f" State Space:")
|
| 74 |
+
lines.append(f" Variables: {result.num_variables}")
|
| 75 |
+
lines.append(f" Total states: {result.total_states:,}")
|
| 76 |
+
lines.append(f" Violations: {result.num_violations:,}")
|
| 77 |
+
lines.append(
|
| 78 |
+
f" Safe states: "
|
| 79 |
+
f"{result.total_states - result.num_violations:,}"
|
| 80 |
+
)
|
| 81 |
+
lines.append(f"{'─' * w}")
|
| 82 |
+
lines.append(f" Quantum Circuit:")
|
| 83 |
+
lines.append(f" Grover iter: {result.grover_iterations}")
|
| 84 |
+
lines.append(f" Circuit depth: {result.circuit_depth}")
|
| 85 |
+
lines.append(f" Gate count: {result.circuit_gate_count}")
|
| 86 |
+
lines.append(f"{'─' * w}")
|
| 87 |
+
lines.append(f" Performance:")
|
| 88 |
+
lines.append(f" Quantum: {result.quantum_time_ms:.2f} ms")
|
| 89 |
+
lines.append(f" Classical: {result.classical_time_ms:.2f} ms")
|
| 90 |
+
lines.append(
|
| 91 |
+
f" Speedup: {result.speedup_factor:.1f}× (theoretical)"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
if result.witnesses:
|
| 95 |
+
lines.append(f"{'─' * w}")
|
| 96 |
+
lines.append(
|
| 97 |
+
f" Top Failure Witnesses ({len(result.witnesses)}):"
|
| 98 |
+
)
|
| 99 |
+
for i, w_ in enumerate(result.witnesses[:3]):
|
| 100 |
+
lines.append(f"\n Witness #{i + 1}:")
|
| 101 |
+
lines.append(f" Bitstring: {w_.bitstring}")
|
| 102 |
+
for vc in w_.violated_constraints:
|
| 103 |
+
lines.append(f" ⛔ {vc.description}")
|
| 104 |
+
|
| 105 |
+
lines.append(f"\n{'━' * w}")
|
| 106 |
+
return "\n".join(lines)
|
| 107 |
+
|
| 108 |
+
@staticmethod
|
| 109 |
+
def plot_measurements(
|
| 110 |
+
counts: Dict[str, int],
|
| 111 |
+
violation_states: Optional[List[int]] = None,
|
| 112 |
+
save_path: str = "qsvaps_results.png",
|
| 113 |
+
):
|
| 114 |
+
"""Plot measurement histogram with violations highlighted.
|
| 115 |
+
|
| 116 |
+
Requires matplotlib. Saves to disk and displays if possible.
|
| 117 |
+
"""
|
| 118 |
+
try:
|
| 119 |
+
import matplotlib.pyplot as plt
|
| 120 |
+
from matplotlib.patches import Patch
|
| 121 |
+
except ImportError:
|
| 122 |
+
print("⚠️ matplotlib not available — skipping plot")
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
# Determine violation bitstrings
|
| 126 |
+
violation_bs = set()
|
| 127 |
+
if violation_states and counts:
|
| 128 |
+
n_bits = len(next(iter(counts)))
|
| 129 |
+
violation_bs = {
|
| 130 |
+
format(s, f"0{n_bits}b") for s in violation_states
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
# Top 20 by count
|
| 134 |
+
sorted_items = sorted(
|
| 135 |
+
counts.items(), key=lambda x: x[1], reverse=True
|
| 136 |
+
)[:20]
|
| 137 |
+
labels = [item[0] for item in sorted_items]
|
| 138 |
+
values = [item[1] for item in sorted_items]
|
| 139 |
+
colors = [
|
| 140 |
+
"#ef4444" if lbl in violation_bs else "#6366f1"
|
| 141 |
+
for lbl in labels
|
| 142 |
+
]
|
| 143 |
+
|
| 144 |
+
fig, ax = plt.subplots(figsize=(12, 5))
|
| 145 |
+
ax.bar(labels, values, color=colors, edgecolor="white", linewidth=0.5)
|
| 146 |
+
ax.set_xlabel("Measured State (bitstring)", fontsize=11)
|
| 147 |
+
ax.set_ylabel("Count", fontsize=11)
|
| 148 |
+
ax.set_title(
|
| 149 |
+
"Grover Search — Measurement Results",
|
| 150 |
+
fontsize=14,
|
| 151 |
+
fontweight="bold",
|
| 152 |
+
)
|
| 153 |
+
plt.xticks(rotation=45, ha="right", fontsize=9)
|
| 154 |
+
|
| 155 |
+
legend_elements = [
|
| 156 |
+
Patch(facecolor="#ef4444", label="Violation state"),
|
| 157 |
+
Patch(facecolor="#6366f1", label="Valid state"),
|
| 158 |
+
]
|
| 159 |
+
ax.legend(handles=legend_elements, loc="upper right")
|
| 160 |
+
|
| 161 |
+
plt.tight_layout()
|
| 162 |
+
plt.savefig(save_path, dpi=150, bbox_inches="tight")
|
| 163 |
+
print(f"📊 Plot saved to {save_path}")
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
plt.show()
|
| 167 |
+
except Exception:
|
| 168 |
+
pass
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
qiskit>=1.0.0
|
| 2 |
+
qiskit-aer>=0.13.0
|
| 3 |
+
numpy>=1.24.0
|
| 4 |
+
matplotlib>=3.7.0
|
| 5 |
+
openai>=1.0.0
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_constraints.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the constraint engine."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from qsvaps.models import (
|
| 5 |
+
Plan,
|
| 6 |
+
PlanAction,
|
| 7 |
+
PlanConstraint,
|
| 8 |
+
ResourceConstraint,
|
| 9 |
+
ConstraintType,
|
| 10 |
+
)
|
| 11 |
+
from qsvaps.constraint_engine import ConstraintEngine
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _simple_plan():
|
| 15 |
+
"""Two actions with a dependency."""
|
| 16 |
+
return Plan(
|
| 17 |
+
name="Simple",
|
| 18 |
+
actions=[
|
| 19 |
+
PlanAction(name="a", can_fail=True),
|
| 20 |
+
PlanAction(name="b", can_fail=True),
|
| 21 |
+
],
|
| 22 |
+
dependencies=[("a", "b")],
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _resource_plan():
|
| 27 |
+
"""Two actions sharing a rate-limited resource."""
|
| 28 |
+
return Plan(
|
| 29 |
+
name="Resource",
|
| 30 |
+
actions=[
|
| 31 |
+
PlanAction(name="x", resources=["api"]),
|
| 32 |
+
PlanAction(name="y", resources=["api"]),
|
| 33 |
+
],
|
| 34 |
+
resource_constraints=[
|
| 35 |
+
ResourceConstraint("api", max_concurrent=1),
|
| 36 |
+
],
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _must_succeed_plan():
|
| 41 |
+
"""One action that must succeed."""
|
| 42 |
+
return Plan(
|
| 43 |
+
name="Must Succeed",
|
| 44 |
+
actions=[
|
| 45 |
+
PlanAction(name="critical", can_fail=False),
|
| 46 |
+
],
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class TestVariableAssignment:
|
| 51 |
+
def test_success_variables(self):
|
| 52 |
+
engine = ConstraintEngine(_simple_plan())
|
| 53 |
+
assert "s_a" in engine.var_mapping.variables
|
| 54 |
+
assert "s_b" in engine.var_mapping.variables
|
| 55 |
+
assert engine.var_mapping.num_variables == 2
|
| 56 |
+
|
| 57 |
+
def test_parallel_variables(self):
|
| 58 |
+
engine = ConstraintEngine(_resource_plan())
|
| 59 |
+
# s_x, s_y, p_x_y
|
| 60 |
+
assert engine.var_mapping.num_variables == 3
|
| 61 |
+
assert "p_x_y" in engine.var_mapping.variables
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class TestDependencyConstraints:
|
| 65 |
+
def test_generates_constraint(self):
|
| 66 |
+
engine = ConstraintEngine(_simple_plan())
|
| 67 |
+
constraints = engine.extract_constraints()
|
| 68 |
+
dep_constraints = [
|
| 69 |
+
c for c in constraints
|
| 70 |
+
if c.constraint_type == ConstraintType.DEPENDENCY
|
| 71 |
+
]
|
| 72 |
+
assert len(dep_constraints) == 1
|
| 73 |
+
assert "requires" in dep_constraints[0].description.lower() or \
|
| 74 |
+
"succeed" in dep_constraints[0].description.lower()
|
| 75 |
+
|
| 76 |
+
def test_dependency_logic(self):
|
| 77 |
+
"""b succeeding without a should violate the dependency."""
|
| 78 |
+
engine = ConstraintEngine(_simple_plan())
|
| 79 |
+
constraints = engine.extract_constraints()
|
| 80 |
+
|
| 81 |
+
# State: a=0, b=1 → b succeeds but a didn't → violation
|
| 82 |
+
# x0=s_a=0, x1=s_b=1 → state = 0b10 = 2
|
| 83 |
+
is_viol, violated = engine.evaluate_state(2, constraints)
|
| 84 |
+
assert is_viol
|
| 85 |
+
|
| 86 |
+
# State: a=1, b=1 → both succeed → no violation
|
| 87 |
+
is_viol, violated = engine.evaluate_state(3, constraints)
|
| 88 |
+
assert not is_viol
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class TestResourceConstraints:
|
| 92 |
+
def test_generates_constraint(self):
|
| 93 |
+
engine = ConstraintEngine(_resource_plan())
|
| 94 |
+
constraints = engine.extract_constraints()
|
| 95 |
+
res_constraints = [
|
| 96 |
+
c for c in constraints
|
| 97 |
+
if c.constraint_type == ConstraintType.RESOURCE
|
| 98 |
+
]
|
| 99 |
+
assert len(res_constraints) == 1
|
| 100 |
+
|
| 101 |
+
def test_parallel_conflict(self):
|
| 102 |
+
"""Both actions succeed + run in parallel → violation."""
|
| 103 |
+
engine = ConstraintEngine(_resource_plan())
|
| 104 |
+
constraints = engine.extract_constraints()
|
| 105 |
+
|
| 106 |
+
# Variables: s_x=0, s_y=1, p_x_y=2
|
| 107 |
+
# State: s_x=1, s_y=1, p_x_y=1 → 0b111 = 7
|
| 108 |
+
is_viol, _ = engine.evaluate_state(7, constraints)
|
| 109 |
+
assert is_viol
|
| 110 |
+
|
| 111 |
+
# State: s_x=1, s_y=1, p_x_y=0 → not parallel → OK
|
| 112 |
+
is_viol, _ = engine.evaluate_state(3, constraints)
|
| 113 |
+
assert not is_viol
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class TestCompletionConstraints:
|
| 117 |
+
def test_must_succeed(self):
|
| 118 |
+
engine = ConstraintEngine(_must_succeed_plan())
|
| 119 |
+
constraints = engine.extract_constraints()
|
| 120 |
+
|
| 121 |
+
comp = [
|
| 122 |
+
c for c in constraints
|
| 123 |
+
if c.constraint_type == ConstraintType.COMPLETION
|
| 124 |
+
]
|
| 125 |
+
assert len(comp) == 1
|
| 126 |
+
|
| 127 |
+
# State 0: critical fails → violation
|
| 128 |
+
is_viol, _ = engine.evaluate_state(0, constraints)
|
| 129 |
+
assert is_viol
|
| 130 |
+
|
| 131 |
+
# State 1: critical succeeds → OK
|
| 132 |
+
is_viol, _ = engine.evaluate_state(1, constraints)
|
| 133 |
+
assert not is_viol
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class TestFindAllViolations:
|
| 137 |
+
def test_simple_plan_violations(self):
|
| 138 |
+
engine = ConstraintEngine(_simple_plan())
|
| 139 |
+
constraints = engine.extract_constraints()
|
| 140 |
+
violations = engine.find_all_violations(constraints)
|
| 141 |
+
|
| 142 |
+
# 2 qubits → 4 states. The only violation is state 2 (b=1, a=0)
|
| 143 |
+
assert 2 in violations
|
| 144 |
+
assert 0 not in violations # Both fail — no dependency violated
|
| 145 |
+
assert 1 not in violations # a=1, b=0 — OK
|
| 146 |
+
assert 3 not in violations # Both succeed — OK
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class TestBooleanEvaluation:
|
| 150 |
+
def test_simple_expressions(self):
|
| 151 |
+
assert ConstraintEngine._eval_bool_expr(
|
| 152 |
+
"x0 or x1", {"x0": True, "x1": False}
|
| 153 |
+
) is True
|
| 154 |
+
assert ConstraintEngine._eval_bool_expr(
|
| 155 |
+
"x0 and x1", {"x0": True, "x1": False}
|
| 156 |
+
) is False
|
| 157 |
+
assert ConstraintEngine._eval_bool_expr(
|
| 158 |
+
"not x0", {"x0": False}
|
| 159 |
+
) is True
|
| 160 |
+
|
| 161 |
+
def test_complex_expression(self):
|
| 162 |
+
assert ConstraintEngine._eval_bool_expr(
|
| 163 |
+
"not (x0 and x1 and x2)",
|
| 164 |
+
{"x0": True, "x1": True, "x2": True},
|
| 165 |
+
) is False
|
| 166 |
+
|
| 167 |
+
def test_variable_ordering(self):
|
| 168 |
+
"""Ensure x10 doesn't get partially replaced by x1."""
|
| 169 |
+
result = ConstraintEngine._eval_bool_expr(
|
| 170 |
+
"x1 and x10",
|
| 171 |
+
{"x1": True, "x10": False},
|
| 172 |
+
)
|
| 173 |
+
assert result is False
|
tests/test_grover.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the Grover search engine."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from qsvaps.grover_search import GroverSearchEngine, SearchResult
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestGroverSearch:
|
| 8 |
+
def setup_method(self):
|
| 9 |
+
self.engine = GroverSearchEngine()
|
| 10 |
+
|
| 11 |
+
def test_empty_violations(self):
|
| 12 |
+
result = self.engine.search([], 3)
|
| 13 |
+
assert result.violation_states_found == []
|
| 14 |
+
assert result.num_iterations == 0
|
| 15 |
+
|
| 16 |
+
def test_finds_single_violation(self):
|
| 17 |
+
"""Grover should amplify the single marked state."""
|
| 18 |
+
violations = [5] # State |101⟩ in 3 qubits
|
| 19 |
+
result = self.engine.search(violations, 3, shots=2048)
|
| 20 |
+
|
| 21 |
+
assert len(result.violation_states_found) >= 1
|
| 22 |
+
assert 5 in result.violation_states_found
|
| 23 |
+
assert result.num_iterations > 0
|
| 24 |
+
|
| 25 |
+
def test_finds_multiple_violations(self):
|
| 26 |
+
"""Grover should find multiple marked states."""
|
| 27 |
+
violations = [0, 3, 7] # Three violations in 3 qubits
|
| 28 |
+
result = self.engine.search(violations, 3, shots=4096)
|
| 29 |
+
|
| 30 |
+
found = set(result.violation_states_found)
|
| 31 |
+
# Should find at least some of the violations
|
| 32 |
+
assert len(found & set(violations)) >= 1
|
| 33 |
+
|
| 34 |
+
def test_two_qubit_search(self):
|
| 35 |
+
"""Simple 2-qubit case: mark state |10⟩ = 2."""
|
| 36 |
+
violations = [2]
|
| 37 |
+
result = self.engine.search(violations, 2, shots=2048)
|
| 38 |
+
|
| 39 |
+
# With 1 violation in 4 states, Grover should find it with high prob
|
| 40 |
+
assert 2 in result.violation_states_found
|
| 41 |
+
|
| 42 |
+
def test_result_structure(self):
|
| 43 |
+
result = self.engine.search([1], 2, shots=100)
|
| 44 |
+
assert isinstance(result, SearchResult)
|
| 45 |
+
assert result.num_shots == 100
|
| 46 |
+
assert result.circuit_depth > 0
|
| 47 |
+
assert result.circuit_gate_count > 0
|
| 48 |
+
assert result.execution_time_ms > 0
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TestClassicalSearch:
|
| 52 |
+
def setup_method(self):
|
| 53 |
+
self.engine = GroverSearchEngine()
|
| 54 |
+
|
| 55 |
+
def test_finds_all(self):
|
| 56 |
+
target = {2, 5, 7}
|
| 57 |
+
found, time_ms = self.engine.classical_search(
|
| 58 |
+
lambda s: s in target, 3
|
| 59 |
+
)
|
| 60 |
+
assert set(found) == target
|
| 61 |
+
assert time_ms >= 0
|
| 62 |
+
|
| 63 |
+
def test_finds_none(self):
|
| 64 |
+
found, _ = self.engine.classical_search(lambda s: False, 3)
|
| 65 |
+
assert found == []
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class TestBenchmark:
|
| 69 |
+
def test_benchmark_returns_metrics(self):
|
| 70 |
+
engine = GroverSearchEngine()
|
| 71 |
+
violations = [1, 2]
|
| 72 |
+
result = engine.benchmark(violations, 2, shots=512)
|
| 73 |
+
|
| 74 |
+
assert "num_qubits" in result
|
| 75 |
+
assert "total_states" in result
|
| 76 |
+
assert "quantum_time_ms" in result
|
| 77 |
+
assert "classical_time_ms" in result
|
| 78 |
+
assert "theoretical_speedup" in result
|
| 79 |
+
assert "detection_rate" in result
|
| 80 |
+
assert result["total_states"] == 4
|
| 81 |
+
assert result["num_violations"] == 2
|
tests/test_models.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for QSVAPS data models."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from qsvaps.models import (
|
| 5 |
+
Plan,
|
| 6 |
+
PlanAction,
|
| 7 |
+
PlanConstraint,
|
| 8 |
+
ResourceConstraint,
|
| 9 |
+
ConstraintType,
|
| 10 |
+
FailureWitness,
|
| 11 |
+
VerificationResult,
|
| 12 |
+
VariableMapping,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestPlanAction:
|
| 17 |
+
def test_basic_creation(self):
|
| 18 |
+
action = PlanAction(name="fetch", description="Fetch data")
|
| 19 |
+
assert action.name == "fetch"
|
| 20 |
+
assert action.description == "Fetch data"
|
| 21 |
+
assert action.can_fail is True
|
| 22 |
+
assert action.fallback is None
|
| 23 |
+
assert action.resources == []
|
| 24 |
+
|
| 25 |
+
def test_with_resources(self):
|
| 26 |
+
action = PlanAction(
|
| 27 |
+
name="process",
|
| 28 |
+
resources=["gpu", "memory"],
|
| 29 |
+
can_fail=False,
|
| 30 |
+
)
|
| 31 |
+
assert action.resources == ["gpu", "memory"]
|
| 32 |
+
assert action.can_fail is False
|
| 33 |
+
|
| 34 |
+
def test_with_fallback(self):
|
| 35 |
+
action = PlanAction(name="main", fallback="backup")
|
| 36 |
+
assert action.fallback == "backup"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class TestPlan:
|
| 40 |
+
def test_basic_plan(self):
|
| 41 |
+
plan = Plan(
|
| 42 |
+
name="Test Plan",
|
| 43 |
+
actions=[
|
| 44 |
+
PlanAction(name="a"),
|
| 45 |
+
PlanAction(name="b"),
|
| 46 |
+
],
|
| 47 |
+
dependencies=[("a", "b")],
|
| 48 |
+
)
|
| 49 |
+
assert len(plan.actions) == 2
|
| 50 |
+
assert plan.action_names == ["a", "b"]
|
| 51 |
+
|
| 52 |
+
def test_get_action(self):
|
| 53 |
+
plan = Plan(
|
| 54 |
+
name="Test",
|
| 55 |
+
actions=[PlanAction(name="x"), PlanAction(name="y")],
|
| 56 |
+
)
|
| 57 |
+
assert plan.get_action("x").name == "x"
|
| 58 |
+
assert plan.get_action("z") is None
|
| 59 |
+
|
| 60 |
+
def test_get_action_index(self):
|
| 61 |
+
plan = Plan(
|
| 62 |
+
name="Test",
|
| 63 |
+
actions=[PlanAction(name="a"), PlanAction(name="b")],
|
| 64 |
+
)
|
| 65 |
+
assert plan.get_action_index("a") == 0
|
| 66 |
+
assert plan.get_action_index("b") == 1
|
| 67 |
+
assert plan.get_action_index("c") == -1
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class TestVariableMapping:
|
| 71 |
+
def test_num_variables(self):
|
| 72 |
+
vm = VariableMapping(
|
| 73 |
+
variables={"s_a": 0, "s_b": 1},
|
| 74 |
+
descriptions={"s_a": "A succeeds", "s_b": "B succeeds"},
|
| 75 |
+
)
|
| 76 |
+
assert vm.num_variables == 2
|
| 77 |
+
|
| 78 |
+
def test_get_var_name(self):
|
| 79 |
+
vm = VariableMapping(variables={"s_a": 0, "s_b": 1})
|
| 80 |
+
assert vm.get_var_name(0) == "s_a"
|
| 81 |
+
assert vm.get_var_name(1) == "s_b"
|
| 82 |
+
assert vm.get_var_name(99) == "x99"
|
| 83 |
+
|
| 84 |
+
def test_get_description(self):
|
| 85 |
+
vm = VariableMapping(
|
| 86 |
+
variables={"s_a": 0},
|
| 87 |
+
descriptions={"s_a": "Action A succeeds"},
|
| 88 |
+
)
|
| 89 |
+
assert vm.get_description(0) == "Action A succeeds"
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class TestFailureWitness:
|
| 93 |
+
def test_creation(self):
|
| 94 |
+
constraint = PlanConstraint(
|
| 95 |
+
expression="x0",
|
| 96 |
+
description="A must succeed",
|
| 97 |
+
constraint_type=ConstraintType.COMPLETION,
|
| 98 |
+
)
|
| 99 |
+
witness = FailureWitness(
|
| 100 |
+
assignment={"s_a": False},
|
| 101 |
+
violated_constraints=[constraint],
|
| 102 |
+
bitstring="0",
|
| 103 |
+
measurement_count=42,
|
| 104 |
+
explanation="A failed",
|
| 105 |
+
)
|
| 106 |
+
assert witness.measurement_count == 42
|
| 107 |
+
assert len(witness.violated_constraints) == 1
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class TestVerificationResult:
|
| 111 |
+
def test_safe_result(self):
|
| 112 |
+
plan = Plan(name="Safe", actions=[])
|
| 113 |
+
result = VerificationResult(
|
| 114 |
+
plan=plan,
|
| 115 |
+
is_safe=True,
|
| 116 |
+
witnesses=[],
|
| 117 |
+
num_variables=3,
|
| 118 |
+
num_violations=0,
|
| 119 |
+
total_states=8,
|
| 120 |
+
grover_iterations=0,
|
| 121 |
+
)
|
| 122 |
+
assert result.is_safe
|
| 123 |
+
assert result.num_violations == 0
|
tests/test_oracle.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the quantum oracle builder."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
import numpy as np
|
| 5 |
+
from qiskit import QuantumCircuit
|
| 6 |
+
from qiskit_aer import AerSimulator
|
| 7 |
+
|
| 8 |
+
from qsvaps.oracle_builder import OracleBuilder
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestPhaseOracle:
|
| 12 |
+
def test_empty_violations(self):
|
| 13 |
+
oracle = OracleBuilder.build_phase_oracle([], 3)
|
| 14 |
+
assert oracle.num_qubits == 3
|
| 15 |
+
assert oracle.size() == 0 # No gates
|
| 16 |
+
|
| 17 |
+
def test_single_violation(self):
|
| 18 |
+
"""Oracle should flip phase of exactly one state."""
|
| 19 |
+
oracle = OracleBuilder.build_phase_oracle([0], 2)
|
| 20 |
+
assert oracle.num_qubits == 2
|
| 21 |
+
assert oracle.size() > 0
|
| 22 |
+
|
| 23 |
+
def test_oracle_correctness(self):
|
| 24 |
+
"""Verify oracle flips phase of marked states using statevector."""
|
| 25 |
+
from qiskit_aer import AerSimulator
|
| 26 |
+
|
| 27 |
+
num_qubits = 2
|
| 28 |
+
violations = [1, 3] # States |01⟩ and |11⟩
|
| 29 |
+
|
| 30 |
+
oracle = OracleBuilder.build_phase_oracle(violations, num_qubits)
|
| 31 |
+
|
| 32 |
+
# Create test circuit: H → Oracle → measure statevector
|
| 33 |
+
qc = QuantumCircuit(num_qubits)
|
| 34 |
+
qc.h(range(num_qubits))
|
| 35 |
+
qc.compose(oracle, inplace=True)
|
| 36 |
+
qc.save_statevector()
|
| 37 |
+
|
| 38 |
+
sim = AerSimulator(method="statevector")
|
| 39 |
+
result = sim.run(qc).result()
|
| 40 |
+
sv = result.get_statevector(qc)
|
| 41 |
+
amplitudes = np.array(sv)
|
| 42 |
+
|
| 43 |
+
# After H|0⟩, all amplitudes are +0.5
|
| 44 |
+
# Oracle should flip signs of violation states
|
| 45 |
+
for i in range(2**num_qubits):
|
| 46 |
+
expected_sign = -1 if i in violations else 1
|
| 47 |
+
assert np.isclose(
|
| 48 |
+
amplitudes[i].real, expected_sign * 0.5, atol=1e-8
|
| 49 |
+
), f"State {i}: expected sign {expected_sign}"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TestDiffuser:
|
| 53 |
+
def test_creates_circuit(self):
|
| 54 |
+
diffuser = OracleBuilder.build_diffuser(3)
|
| 55 |
+
assert diffuser.num_qubits == 3
|
| 56 |
+
assert diffuser.size() > 0
|
| 57 |
+
|
| 58 |
+
def test_single_qubit(self):
|
| 59 |
+
diffuser = OracleBuilder.build_diffuser(1)
|
| 60 |
+
assert diffuser.num_qubits == 1
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class TestOptimalIterations:
|
| 64 |
+
def test_no_violations(self):
|
| 65 |
+
assert OracleBuilder.optimal_iterations(16, 0) == 0
|
| 66 |
+
|
| 67 |
+
def test_all_violations(self):
|
| 68 |
+
assert OracleBuilder.optimal_iterations(16, 16) == 0
|
| 69 |
+
|
| 70 |
+
def test_single_violation(self):
|
| 71 |
+
# π/4 × √(16/1) = π/4 × 4 ≈ 3.14 → 3
|
| 72 |
+
k = OracleBuilder.optimal_iterations(16, 1)
|
| 73 |
+
assert k == 3
|
| 74 |
+
|
| 75 |
+
def test_many_violations(self):
|
| 76 |
+
# π/4 × √(16/8) = π/4 × √2 ≈ 1.11 → 1
|
| 77 |
+
k = OracleBuilder.optimal_iterations(16, 8)
|
| 78 |
+
assert k == 1
|
| 79 |
+
|
| 80 |
+
def test_minimum_one(self):
|
| 81 |
+
k = OracleBuilder.optimal_iterations(4, 3)
|
| 82 |
+
assert k >= 1
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class TestGroverCircuit:
|
| 86 |
+
def test_builds_circuit(self):
|
| 87 |
+
oracle = OracleBuilder.build_phase_oracle([0], 2)
|
| 88 |
+
diffuser = OracleBuilder.build_diffuser(2)
|
| 89 |
+
qc = OracleBuilder.build_grover_circuit(oracle, diffuser, 2, 1)
|
| 90 |
+
|
| 91 |
+
assert qc.num_qubits == 2
|
| 92 |
+
assert qc.num_clbits == 2 # Measurement bits
|
| 93 |
+
|
| 94 |
+
def test_circuit_runs(self):
|
| 95 |
+
"""Ensure the assembled circuit executes without error."""
|
| 96 |
+
oracle = OracleBuilder.build_phase_oracle([2], 2)
|
| 97 |
+
diffuser = OracleBuilder.build_diffuser(2)
|
| 98 |
+
qc = OracleBuilder.build_grover_circuit(oracle, diffuser, 2, 1)
|
| 99 |
+
|
| 100 |
+
sim = AerSimulator()
|
| 101 |
+
job = sim.run(qc, shots=100)
|
| 102 |
+
counts = job.result().get_counts(qc)
|
| 103 |
+
assert len(counts) > 0
|
tests/test_verifier.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the plan verifier (end-to-end pipeline)."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
from qsvaps.models import (
|
| 5 |
+
Plan,
|
| 6 |
+
PlanAction,
|
| 7 |
+
PlanConstraint,
|
| 8 |
+
ResourceConstraint,
|
| 9 |
+
ConstraintType,
|
| 10 |
+
)
|
| 11 |
+
from qsvaps.verifier import PlanVerifier
|
| 12 |
+
from qsvaps.llm_interface import LLMInterface
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _flawed_plan():
|
| 16 |
+
"""Plan with a dependency violation opportunity."""
|
| 17 |
+
return Plan(
|
| 18 |
+
name="Flawed",
|
| 19 |
+
actions=[
|
| 20 |
+
PlanAction(name="a", can_fail=True),
|
| 21 |
+
PlanAction(name="b", can_fail=True),
|
| 22 |
+
],
|
| 23 |
+
dependencies=[("a", "b")],
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _safe_plan():
|
| 28 |
+
"""A plan where nothing can fail."""
|
| 29 |
+
return Plan(
|
| 30 |
+
name="Safe",
|
| 31 |
+
actions=[
|
| 32 |
+
PlanAction(name="x", can_fail=False),
|
| 33 |
+
PlanAction(name="y", can_fail=False),
|
| 34 |
+
],
|
| 35 |
+
dependencies=[("x", "y")],
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class TestVerify:
|
| 40 |
+
def test_detects_violations(self):
|
| 41 |
+
verifier = PlanVerifier(shots=1024)
|
| 42 |
+
result = verifier.verify(_flawed_plan())
|
| 43 |
+
|
| 44 |
+
assert not result.is_safe
|
| 45 |
+
assert result.num_violations > 0
|
| 46 |
+
assert result.total_states == 4 # 2 variables
|
| 47 |
+
assert len(result.witnesses) > 0
|
| 48 |
+
|
| 49 |
+
def test_safe_plan_passes(self):
|
| 50 |
+
verifier = PlanVerifier(shots=1024)
|
| 51 |
+
result = verifier.verify(_safe_plan())
|
| 52 |
+
|
| 53 |
+
# With can_fail=False, completion constraints require both
|
| 54 |
+
# Only state 11 (both succeed) is valid → 3 violations
|
| 55 |
+
# But these are expected violations because the constraints are
|
| 56 |
+
# about what MUST happen, not about what CAN happen
|
| 57 |
+
# The plan has mandatory completion → state 00, 01, 10 all violate
|
| 58 |
+
assert result.num_violations == 3
|
| 59 |
+
assert result.total_states == 4
|
| 60 |
+
|
| 61 |
+
def test_witness_has_explanation(self):
|
| 62 |
+
verifier = PlanVerifier(shots=1024)
|
| 63 |
+
result = verifier.verify(_flawed_plan())
|
| 64 |
+
|
| 65 |
+
if result.witnesses:
|
| 66 |
+
w = result.witnesses[0]
|
| 67 |
+
assert w.bitstring
|
| 68 |
+
assert len(w.violated_constraints) > 0
|
| 69 |
+
|
| 70 |
+
def test_verbose_mode_no_crash(self, capsys):
|
| 71 |
+
"""Verbose mode should print without crashing."""
|
| 72 |
+
verifier = PlanVerifier(shots=512)
|
| 73 |
+
result = verifier.verify(_flawed_plan(), verbose=True)
|
| 74 |
+
captured = capsys.readouterr()
|
| 75 |
+
assert "QSVAPS" in captured.out
|
| 76 |
+
|
| 77 |
+
def test_quantum_metrics(self):
|
| 78 |
+
verifier = PlanVerifier(shots=1024)
|
| 79 |
+
result = verifier.verify(_flawed_plan())
|
| 80 |
+
|
| 81 |
+
assert result.grover_iterations >= 0
|
| 82 |
+
assert result.quantum_time_ms >= 0
|
| 83 |
+
assert result.circuit_depth >= 0
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class TestVerifyAndRepair:
|
| 87 |
+
def test_repair_loop_runs(self):
|
| 88 |
+
mock_llm = LLMInterface(mock=True)
|
| 89 |
+
verifier = PlanVerifier(
|
| 90 |
+
llm=mock_llm, max_repair_iterations=2, shots=1024
|
| 91 |
+
)
|
| 92 |
+
results = verifier.verify_and_repair(_flawed_plan())
|
| 93 |
+
|
| 94 |
+
assert len(results) >= 1
|
| 95 |
+
# First result should be unsafe
|
| 96 |
+
assert not results[0].is_safe
|
| 97 |
+
|
| 98 |
+
def test_repair_without_llm(self):
|
| 99 |
+
"""Without LLM, should return single result."""
|
| 100 |
+
verifier = PlanVerifier(max_repair_iterations=2, shots=1024)
|
| 101 |
+
results = verifier.verify_and_repair(_flawed_plan())
|
| 102 |
+
assert len(results) == 1
|
| 103 |
+
|
| 104 |
+
def test_verbose_repair(self, capsys):
|
| 105 |
+
mock_llm = LLMInterface(mock=True)
|
| 106 |
+
verifier = PlanVerifier(
|
| 107 |
+
llm=mock_llm, max_repair_iterations=1, shots=512
|
| 108 |
+
)
|
| 109 |
+
results = verifier.verify_and_repair(_flawed_plan(), verbose=True)
|
| 110 |
+
captured = capsys.readouterr()
|
| 111 |
+
assert "QSVAPS" in captured.out
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class TestEdgeCases:
|
| 115 |
+
def test_empty_plan(self):
|
| 116 |
+
plan = Plan(name="Empty", actions=[])
|
| 117 |
+
verifier = PlanVerifier(shots=512)
|
| 118 |
+
result = verifier.verify(plan)
|
| 119 |
+
# No variables → 1 state (trivial), no constraints → safe
|
| 120 |
+
assert result.is_safe
|
| 121 |
+
|
| 122 |
+
def test_single_action_plan(self):
|
| 123 |
+
plan = Plan(
|
| 124 |
+
name="Single",
|
| 125 |
+
actions=[PlanAction(name="only", can_fail=False)],
|
| 126 |
+
)
|
| 127 |
+
verifier = PlanVerifier(shots=512)
|
| 128 |
+
result = verifier.verify(plan)
|
| 129 |
+
# 1 variable, completion constraint → state 0 violates
|
| 130 |
+
assert result.num_violations == 1
|
| 131 |
+
assert result.total_states == 2
|