diff --git a/README.md b/README.md index a12fbe0d..f219dd60 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,17 @@ This source code is licensed under the Apache License Version 2.0
pyqpanda-algorithm 是由本源量子(Origin Quantum)开发的量子算法软件包,旨在为量子计算开发者提供一套标准化、模块化、高性能的基础算法库。该库集成了多种在金融、机器学习、组合优化、科学计算等领域广泛应用的量子算法,帮助用户快速实现从理论到代码的转化,提升开发效率并确保算法在不同量子平台上的可移植性。 -软件包官网: [https://qcloud.originqc.com.cn/zh/programming/pyqpanda-algorithm] +软件包官网: [https://qcloud.originqc.com.cn/zh/programming/pyqpanda-algorithm] + +### QSEncode-Insight + +新增的 QSEncode-Insight 根据保真度预算、态制备兼容性和实际编译资源, +推荐压缩或明确拒绝无收益的压缩,同时保持原 `QSpare_Code` 接口不变。 +该创新应用复用 PyQPanda3 已有态制备原语;原创贡献是误差预算、能力过滤、 +编译资源审计、确定性选择、语义验证与拒绝压缩组成的完整工具链, +不宣称重新发明底层态制备算法。 +使用说明、CLI、Notebook 与验证边界见 +[QSEncode-Insight 文档](pyqpanda-algorithm/pyqpanda_alg/QSEncode/README.md)。 ------ diff --git a/README_EN.md b/README_EN.md index d74c953c..0c7c8a93 100644 --- a/README_EN.md +++ b/README_EN.md @@ -3,7 +3,15 @@ ## Introduction pyqpanda-algorithm is a quantum algorithm software package developed by Origin Quantum, designed to provide quantum computing developers with a standardized, modular, and high-performance foundational algorithm library. This library integrates a variety of quantum algorithms widely used in finance, machine learning, combinatorial optimization, scientific computing, and other fields. It helps users quickly translate theories into code, improve development efficiency, and ensure algorithm portability across different quantum platforms. -Official Website: [https://qcloud.originqc.com.cn/zh/programming/pyqpanda-algorithm] +Official Website: [https://qcloud.originqc.com.cn/zh/programming/pyqpanda-algorithm] + +### QSEncode-Insight + +QSEncode-Insight uses a fidelity budget, preparation compatibility, and actual +compiled resources to recommend compression or explicitly refuse an +unprofitable compression. The existing `QSpare_Code` API remains unchanged. +See the [QSEncode-Insight guide](pyqpanda-algorithm/pyqpanda_alg/QSEncode/README.md) +for the CLI, notebook, evidence scope, and reproducible examples. ------ diff --git a/pyqpanda-algorithm/example/QAlgBase/QSEncode_Insight_Demo.ipynb b/pyqpanda-algorithm/example/QAlgBase/QSEncode_Insight_Demo.ipynb new file mode 100644 index 00000000..b1b865ff --- /dev/null +++ b/pyqpanda-algorithm/example/QAlgBase/QSEncode_Insight_Demo.ipynb @@ -0,0 +1,224 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# QSEncode-Insight: Resource-Aware State Preparation\n", + "\n", + "**Audience:** contest reviewers and PyQPanda users. **Prerequisites:** a source checkout with PyQPanda3, NumPy, and SciPy.\n", + "\n", + "**Goals:** compare a refusal with a compression recommendation, inspect candidates and resources, prepare a runnable program, and distinguish standard from audit verification." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Outline\n", + "\n", + "1. Load the sealed Gaussian N=8 example.\n", + "2. Compare Walsh refusal with Fourier compression.\n", + "3. Inspect candidates, resources, and the prepared artifact.\n", + "4. Run a five-repeat audit and check EvidenceScope." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sys\n", + "import numpy as np\n", + "\n", + "# Support Run All from the repository root or this example directory.\n", + "for root in (Path.cwd(), *Path.cwd().parents):\n", + " source_root = root / 'pyqpanda-algorithm'\n", + " if (source_root / 'pyqpanda_alg').is_dir():\n", + " sys.path.insert(0, str(source_root)); break\n", + " if (root / 'pyqpanda_alg').is_dir():\n", + " sys.path.insert(0, str(root)); break\n", + "\n", + "from pyqpanda_alg.QSEncode import QSEncodeInsight\n", + "\n", + "probabilities = np.array([\n", + " 0.0006917643261373052, 0.015724004731018214,\n", + " 0.1261730210273901, 0.3574112099154543,\n", + " 0.3574112099154544, 0.1261730210273902,\n", + " 0.01572400473101823, 0.0006917643261373052,\n", + "])\n", + "float(probabilities.sum()), len(probabilities)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Walsh mode refuses compression\n", + "\n", + "A smaller transformed representation is not automatically a cheaper compiled program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "walsh_engine = QSEncodeInsight(basis='walsh')\n", + "walsh_result = walsh_engine.analyze(probabilities)\n", + "{'decision': walsh_result.selection.decision.value,\n", + " 'k_star': walsh_result.error_budget.k_star,\n", + " 'baseline_2q': walsh_result.selection.baseline_resource.compiled_two_qubit_gates,\n", + " 'baseline_depth': walsh_result.selection.baseline_resource.compiled_depth}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Fourier mode selects sparse preparation\n", + "\n", + "The basis is explicit; v1 does not compare Walsh and Fourier automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fourier_engine = QSEncodeInsight(basis='fourier')\n", + "fourier_result = fourier_engine.analyze(probabilities)\n", + "{'decision': fourier_result.selection.decision.value,\n", + " 'k_star': fourier_result.error_budget.k_star,\n", + " 'winner': fourier_result.selection.selected_candidate_id,\n", + " 'method': fourier_result.selection.method.value}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Candidate table and resource attribution\n", + "\n", + "Incompatible candidates remain visible. Only k=4 is displayed to keep output small." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "candidate_table = []\n", + "for candidate in fourier_result.candidates:\n", + " if candidate.k == 4:\n", + " audit = candidate.resource_audit\n", + " candidate_table.append({\n", + " 'candidate': candidate.candidate_id,\n", + " 'compatible': candidate.capability.compatible,\n", + " 'eligible': candidate.eligible,\n", + " '2q': None if audit is None else audit.compiled_two_qubit_gates,\n", + " 'depth': None if audit is None else audit.compiled_depth,\n", + " 'reason': candidate.eligibility_reason})\n", + "candidate_table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attr = fourier_result.attribution\n", + "{'2q_total_truncation_preparation': (attr.total_two_qubit_difference, attr.truncation_two_qubit_difference, attr.preparation_two_qubit_difference),\n", + " 'depth_total_truncation_preparation': (attr.total_depth_difference, attr.truncation_depth_difference, attr.preparation_depth_difference)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Prepare a runnable program\n", + "\n", + "InsightResult remains JSON-safe; the QProg lives in a separate artifact." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "artifact = fourier_engine.prepare(probabilities, result=fourier_result)\n", + "{'candidate': artifact.selected_candidate_id, 'k': artifact.k,\n", + " 'output_qubits': artifact.output_qubits, 'ancillas': artifact.ancillas,\n", + " 'verification': artifact.verification_status}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Audit verification and EvidenceScope\n", + "\n", + "Standard does not claim compiled semantic certification. Audit reuses the five compiled attempts and requires 5/5 passes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "audit_result = QSEncodeInsight(basis='fourier', verification='audit').analyze(probabilities)\n", + "{'status': audit_result.semantic_verification.status,\n", + " 'attempts': len(audit_result.semantic_verification.attempts),\n", + " 'minimum_fidelity': audit_result.semantic_verification.minimum_fidelity,\n", + " 'evidence_scope': audit_result.evidence_scope.status.value}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercise\n", + "\n", + "Predict the EvidenceScope status for `fidelity_target=0.98`. The answer scaffold below avoids extra compilation during the default Run All; uncomment the analysis when you want to verify it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# outside = QSEncodeInsight(basis='walsh', fidelity_target=0.98).analyze(probabilities)\n", + "# outside.evidence_scope.status.value, outside.evidence_scope.reasons\n", + "expected_status = 'outside_validated_scope'\n", + "expected_status" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Limitations\n", + "\n", + "- These are compiled-resource results, not hardware speedup evidence.\n", + "- The preregistered validation scope is N<=64; Dirichlet inputs were weaker.\n", + "- v1 has no automatic cross-basis selector.\n", + "- Large OriginIR, statevectors, and Locked benchmark artifacts are intentionally not embedded." + ] + } + ], + "metadata": { + "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, + "language_info": {"name": "python", "version": "3.14"} + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/BENCHMARK_EVIDENCE.md b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/BENCHMARK_EVIDENCE.md new file mode 100644 index 00000000..f876ac3f --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/BENCHMARK_EVIDENCE.md @@ -0,0 +1,57 @@ +# QSEncode-Insight benchmark evidence + +## Confirmatory design + +The final generalization benchmark was preregistered before its Locked Test was +run. Its primary data comprised 60 independent distribution-design instances +across five equally weighted families, each evaluated at +`N={8,16,32,64}` and in Walsh and Fourier modes: 480 evaluation cells. + +The primary product metric was `selector-all`: `do_not_compress` cells remained +in the analysis with zero resource gain. Walsh and Fourier were evaluated +separately; the benchmark did not test an automatic cross-basis selector. + +## Locked Test result + +All 480 cells completed under the frozen environment and analysis protocol. + +A presentation-oriented view of the same frozen results, including dimension, +family, refusal, and attribution breakdowns, is available in +[BENCHMARK_VISUAL_SUMMARY.md](BENCHMARK_VISUAL_SUMMARY.md). It is descriptive +only and does not replace the preregistered aggregation below. + +| Basis | Compiled 2q selector-all | Compiled depth selector-all | Gate | +|---|---:|---:|---| +| Walsh | 45.27% | 48.78% | strong pass | +| Fourier | 71.11% | 69.82% | strong pass | + +There were 55 `do_not_compress` decisions among the 480 cells (11.46%). They +were not removed from the main statistic. + +The preregistered hierarchical point estimate determined the gate. Cluster +bootstrap intervals described stability and did not replace the gate rule. + +## Interpretation + +The mechanisms differed by basis: + +- Walsh two-qubit savings came predominantly from preparation-strategy choice; +- Fourier savings combined fidelity-budget truncation with an additional + preparation-strategy contribution. + +The Dirichlet family was much weaker than Gaussian, bimodal, exponential, and +step families. This negative evidence is retained: QSEncode-Insight diagnoses +whether an input is worth compressing rather than claiming that every +distribution is compressible. + +Candidate incompatibility was also common, particularly for DS preparation. +The final recommendation nevertheless required a compatible, correctness- +checked candidate and five successful compiled attempts. Capability filtering +and explicit refusal are therefore core behavior, not cosmetic reporting. + +## Claim boundary + +Within the preregistered N<=64 test scope, Walsh and Fourier modes both met the +predefined compiled-resource gate. These results do not establish quantum +advantage, hardware runtime acceleration, behavior at larger dimensions, or an +automatic best-basis selector. diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/BENCHMARK_VISUAL_SUMMARY.md b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/BENCHMARK_VISUAL_SUMMARY.md new file mode 100644 index 00000000..20e5200f --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/BENCHMARK_VISUAL_SUMMARY.md @@ -0,0 +1,66 @@ +# QSEncode-Insight benchmark visual summary + +This page presents the already frozen Locked Generalization Test. It does not +rerun the benchmark, introduce a new metric, or replace the preregistered gate. +The main design contained 60 independent distribution-design instances and 480 +evaluation cells across five equally weighted families, `N={8,16,32,64}`, and +separately selected Walsh and Fourier modes. + +![Selector-all reductions by dimension](assets/benchmark_by_dimension.svg) + +The dimension view is descriptive: each point is the median over the 60 cells +at that basis and dimension. It must not be substituted for the preregistered +instance → family → basis aggregation. + +| Basis | N | Cells | `DO_NOT_COMPRESS` | Median 2q reduction | Median depth reduction | +|---|---:|---:|---:|---:|---:| +| Walsh | 8 | 60 | 15 | 0% | 10% | +| Walsh | 16 | 60 | 10 | 29% | 33% | +| Walsh | 32 | 60 | 4 | 75% | 73% | +| Walsh | 64 | 60 | 6 | 94% | 92% | +| Fourier | 8 | 60 | 18 | 4% | 6% | +| Fourier | 16 | 60 | 2 | 32% | 32% | +| Fourier | 32 | 60 | 0 | 70% | 66% | +| Fourier | 64 | 60 | 0 | 92% | 90% | + +![Family-level selector-all heatmap](assets/benchmark_family_heatmap.svg) + +The family values are the preregistered family medians. Dirichlet is retained +as negative evidence rather than removed from the pooled result. + +| Basis / endpoint | Gaussian | Bimodal | Exponential | Step | Dirichlet | +|---|---:|---:|---:|---:|---:| +| Walsh 2q | 47.85% | 45.27% | 93.12% | 44.56% | 0.01% | +| Walsh depth | 48.78% | 49.31% | 94.46% | 45.80% | 0.61% | +| Fourier 2q | 90.64% | 89.24% | 71.11% | 39.27% | 12.28% | +| Fourier depth | 90.42% | 88.98% | 69.82% | 37.09% | 12.76% | + +## Refusal and method mix + +- `425/480` cells selected compression; `55/480` (`11.46%`) returned + `DO_NOT_COMPRESS` and contributed zero gain to `selector-all`. +- The 425 compressed winners comprised 134 `amplitude_encode`, 260 + `sparse_isometry`, and 31 `ds_quantum_state_preparation` selections. +- Constructor incompatibilities were preserved and filtered before selection; + they were not counted as successful candidates. + +## Attribution + +Attribution is descriptive over compressed cells and uses `dense_full` as the +denominator. It separates fidelity-budget truncation from the incremental effect +of preparation-strategy choice. + +| Basis / resource | Truncation mean | Preparation mean | Total mean | +|---|---:|---:|---:| +| Walsh compiled 2q | -0.07% | 48.76% | 48.69% | +| Walsh compiled depth | 4.79% | 46.34% | 51.13% | +| Fourier compiled 2q | 41.46% | 16.85% | 58.31% | +| Fourier compiled depth | 41.46% | 16.54% | 57.99% | + +## Claim boundary + +These results support compiled-resource decisions only within the frozen +Python 3.14.2 / PyQPanda3 0.3.5 environment, the five tested families, +`N<=64`, and explicitly selected Walsh or Fourier mode. They do not establish +quantum advantage, hardware runtime acceleration, behavior beyond the tested +scope, or an automatic cross-basis selector. diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/README.md b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/README.md new file mode 100644 index 00000000..80e3588c --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/README.md @@ -0,0 +1,231 @@ +# QSEncode-Insight + +QSEncode-Insight decides whether a probability distribution should actually be +compressed before it is represented by a quantum program. It combines an +explicit fidelity budget, several state-preparation strategies, five-repeat +compiled-resource auditing, and a refusal path when compression is not useful. + +It is a new API alongside the existing `QSpare_Code`; the legacy class and its +behavior remain unchanged. + +## Contribution boundary + +QSEncode-Insight is an innovation-application tool, not a claim of a newly +invented low-level state-preparation decomposition. The circuit constructors +`amplitude_encode`, `sparse_isometry`, and `ds_quantum_state_preparation` are +provided by PyQPanda3. This package contributes the end-to-end decision layer: +canonical input handling, fast transforms, fidelity-budgeted `k*`, capability +filtering, repeated compiled-resource auditing, deterministic selection, +semantic verification, resource attribution, and explicit refusal. + +```text +probability distribution + ↓ +canonicalization and padding + ↓ +explicit Walsh or Fourier mode + ↓ +fidelity target → minimal k* + ↓ +fixed candidate neighborhood + ↓ +preparation capability filter + ↓ +five-repeat compiled-resource audit + ↓ +frozen resource selector + ↓ +COMPRESS / DO_NOT_COMPRESS + ↓ +standard / audit verification +``` + +## Why it exists + +The original QSEncode interface can truncate transformed coefficients with a +fixed cut length. A fixed cut does not express a fidelity requirement, and a +smaller coefficient vector does not guarantee a smaller compiled circuit. +State-preparation methods also have different qubit, ancilla, two-qubit-gate, +and depth costs. Some inputs are simply not worth compressing. + +QSEncode-Insight instead follows this sequence: + +1. derive the minimal retained coefficient count `k*` from a target fidelity; +2. evaluate the frozen neighborhood `{k*-1, k*, k*+1}`; +3. filter incompatible preparation methods; +4. compare real compiled resources under one fixed compiler profile; +5. recommend compression, or explicitly return `do_not_compress`. + +This is compiled-resource evidence. It is not a claim of quantum speedup, +hardware acceleration, or guaranteed savings for every distribution. + +## Source-checkout setup + +This repository is a source checkout. From the repository root, install the +QSEncode-Insight lock file and expose the inner source directory to Python: + +```bash +python -m pip install -r pyqpanda-algorithm/requirements-qseencode-insight.txt +``` + +PowerShell: + +```powershell +$env:PYTHONPATH = (Resolve-Path .\pyqpanda-algorithm).Path +``` + +Bash: + +```bash +export PYTHONPATH="$PWD/pyqpanda-algorithm${PYTHONPATH:+:$PYTHONPATH}" +``` + +Validated contest environment: + +- Python 3.14.2 +- PyQPanda3 0.3.5 +- NumPy 2.4.6 +- SciPy 1.17.1 +- SymPy 1.14.0 +- Matplotlib 3.10.9 + +Submission compatibility was independently rechecked in a clean Python 3.12.10 +environment with the same pinned scientific stack. The QSEncode-Insight suite +passed 237 tests, the full repository suite passed 254 tests, a wheel was built +and installed, and the API, CLI, and notebook Run-All smoke checks passed. The +benchmark claim itself remains tied to the frozen Python 3.14.2 environment +described below. + +For the exact test environment, also install: + +```bash +python -m pip install -r pyqpanda-algorithm/requirements-qseencode-insight-test.txt +``` + +The repository's historical `test/pytest.ini` enables Allure command-line +options. QSEncode-Insight provides a dependency-light test configuration that +does not require Allure: + +```powershell +$env:PYTHONPATH = (Resolve-Path .\pyqpanda-algorithm).Path +python -m pytest -c pyqpanda-algorithm/pytest-qseencode.ini +``` + +```bash +export PYTHONPATH="$PWD/pyqpanda-algorithm${PYTHONPATH:+:$PYTHONPATH}" +python -m pytest -c pyqpanda-algorithm/pytest-qseencode.ini +``` + +## Quick start + +```python +import numpy as np + +from pyqpanda_alg.QSEncode import QSEncodeInsight + +probabilities = np.array([ + 0.0006917643261373052, + 0.015724004731018214, + 0.1261730210273901, + 0.3574112099154543, + 0.3574112099154544, + 0.1261730210273902, + 0.01572400473101823, + 0.0006917643261373052, +]) + +engine = QSEncodeInsight( + basis="fourier", + fidelity_target=0.99, +) +result = engine.analyze(probabilities) + +print(result.selection.decision.value) +print(result.selection.selected_candidate_id) + +artifact = engine.prepare(probabilities, result=result) +print(artifact.output_qubits) +``` + +For this fixed N=8 example, Fourier mode recommends +`compressed__k4__sparse_isometry`. The same distribution in Walsh mode returns +`do_not_compress`, demonstrating that truncation is not automatically useful. + +## Reading a result + +`InsightResult` is a deterministic, JSON-serializable snapshot. Its main +sections are: + +- `InputSummary`: normalization, padding, and input hashes; +- `TransformDiagnostics`: basis convention and Parseval checks; +- `ErrorBudgetResult`: target fidelity, `k*`, and candidate neighborhood; +- `CapabilityReport`: method compatibility and failure reasons; +- `ResourceAudit`: five compiled attempts, medians, ranges, and hashes; +- `SelectionResult`: winner or refusal with primary-resource comparison; +- `SemanticVerification`: standard status or five-repeat audit evidence; +- `EvidenceScope`: whether the run is inside the validated contest scope. + +Use `result.to_dict()` or `result.to_json(indent=2)` for structured reporting. +The result does not embed QProg or full OriginIR text; runnable programs are +returned separately by `prepare()`. + +## Standard and audit verification + +`verification="standard"` is the default. It performs input, transform, +error-budget, logical-preparation, capability, compilation, topology, basis, +resource, and selection checks. It deliberately does **not** perform a compiled +statevector semantic sweep, and reports `not_run_by_standard`. + +`verification="audit"` reuses the same five compiled attempts for the actual +recommendation and certifies all five semantically. Only 5/5 passes produce a +valid audited recommendation. A failed audit does not select a different +method or `k`; `prepare()` blocks the uncertified recommendation unless the +caller explicitly requests a documented dense-baseline fallback. + +## Evidence scope + +`validated_default` currently requires: + +- `fidelity_target=0.99`; +- an explicitly selected `walsh` or `fourier` basis; +- `N` in `{8, 16, 32, 64}`; +- the exact default method order and frozen selector policy; +- PyQPanda3 0.3.5 and the frozen five-repeat compiler profile. + +Other supported configurations can still run, but are labeled +`outside_validated_scope`; they must not be described as benchmark-validated. +There is no automatic Walsh/Fourier selector in v1. + +## CLI + +Analyze an inline JSON list: + +```bash +python -m pyqpanda_alg.QSEncode.cli analyze \ + --basis fourier \ + --fidelity-target 0.99 \ + --verification standard \ + --input-json '[0.1,0.2,0.3,0.4]' +``` + +Use `--input-file probabilities.json` instead of `--input-json` for a UTF-8 +JSON file, and `--pretty` for indented output. + +## Reproducible demo and evidence + +- Notebook: `example/QAlgBase/QSEncode_Insight_Demo.ipynb` +- Benchmark summary: [BENCHMARK_EVIDENCE.md](BENCHMARK_EVIDENCE.md) +- Visual benchmark summary: [BENCHMARK_VISUAL_SUMMARY.md](BENCHMARK_VISUAL_SUMMARY.md) + +The notebook uses only the small sealed N=8 example and does not run the +480-cell benchmark. + +## Limitations + +- The confirmed evidence is limited to the preregistered N<=64 scope. +- Dirichlet-family inputs were substantially weaker than the other main + families in the generalization test. +- Some preparation constructors reject otherwise valid candidates; capability + filtering and fallback behavior are therefore part of the product. +- Compiled gate/depth reductions are not evidence of end-to-end hardware + speedup. diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/__init__.py index 419526bc..603f9a9b 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/__init__.py +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/__init__.py @@ -1,5 +1,81 @@ - - -from .QSEncode import QSpare_Code - -__all__ = ["QSpare_Code"] \ No newline at end of file +from .QSEncode import QSpare_Code +from .config import ( + DEFAULT_METHODS, + SCHEMA_VERSION, + SELECTION_POLICY, + AnalysisConfig, + CompilerConfig, + EvidenceScopeStatus, + InputPolicy, + PreparationMethod, + SelectionDecision, + VerificationLevel, + analysis_config_fingerprint, + canonical_analysis_config_json, +) +from .exceptions import ( + BaselineConstructionError, + ConfigurationError, + InputValidationError, + InternalInvariantError, + QSEncodeInsightError, + ResourceAuditError, + ResultBindingError, + SerializationError, + UncertifiedSelectionError, +) +from .insight import QSEncodeInsight +from .models import ( + AttributionReport, + CandidateResult, + CapabilityReport, + ErrorBudgetResult, + EvidenceScope, + InputSummary, + InsightResult, + PreparationArtifact, + ResourceAudit, + SelectionResult, + SemanticVerification, + SemanticVerificationAttempt, + TransformDiagnostics, +) + +__all__ = [ + "QSpare_Code", + "QSEncodeInsight", + "SCHEMA_VERSION", + "SELECTION_POLICY", + "PreparationMethod", + "DEFAULT_METHODS", + "EvidenceScopeStatus", + "VerificationLevel", + "SelectionDecision", + "InputPolicy", + "CompilerConfig", + "AnalysisConfig", + "analysis_config_fingerprint", + "canonical_analysis_config_json", + "QSEncodeInsightError", + "InputValidationError", + "ConfigurationError", + "BaselineConstructionError", + "ResourceAuditError", + "SerializationError", + "ResultBindingError", + "UncertifiedSelectionError", + "InternalInvariantError", + "InputSummary", + "TransformDiagnostics", + "ErrorBudgetResult", + "CapabilityReport", + "CandidateResult", + "SelectionResult", + "ResourceAudit", + "SemanticVerification", + "SemanticVerificationAttempt", + "AttributionReport", + "EvidenceScope", + "InsightResult", + "PreparationArtifact", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_capability.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_capability.py new file mode 100644 index 00000000..f472216e --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_capability.py @@ -0,0 +1,331 @@ +"""Static and construction capability filters for state preparation.""" + +from __future__ import annotations + +from dataclasses import dataclass +import math +from typing import Any + +import numpy as np +from pyqpanda3.core import CPUQVM + +from .config import DEFAULT_METHODS, PreparationMethod +from .exceptions import InternalInvariantError +from .models import CapabilityReport +from ._preparation import ( + PreparationBuild, + PreparationInput, + build_preparation, + required_qubit_contract, +) + + +LOGICAL_FIDELITY_TOLERANCE = 1e-10 +CAPABILITY_REASON_CODES = frozenset( + { + "compatible", + "unsupported_complex", + "invalid_sparse_keys", + "support_too_dense", + "insufficient_qubits", + "unexpected_output_register", + "constructor_rejected_input", + "logical_fidelity_mismatch", + "unknown_backend_error", + } +) + +_REASONS = { + "compatible": "The method constructed the requested logical state.", + "unsupported_complex": "The method does not support this complex state.", + "invalid_sparse_keys": "Sparse binary keys do not match the logical width.", + "support_too_dense": "The support violates a method-specific density constraint.", + "insufficient_qubits": "The available qubits are below the method requirement.", + "unexpected_output_register": "The constructed output/ancilla metadata is inconsistent.", + "constructor_rejected_input": "The backend constructor rejected the adapted input.", + "logical_fidelity_mismatch": "The constructed logical state failed exact fidelity validation.", + "unknown_backend_error": "The backend or simulator failed unexpectedly.", +} + + +@dataclass(frozen=True, slots=True) +class CapabilityAssessment: + report: CapabilityReport + build: PreparationBuild | None = None + + +def _report( + method: PreparationMethod, + *, + compatible: bool, + reason_code: str, + failure_stage: str | None, + required_qubits: int | None, + ancillas: tuple[int, ...] = (), + output_qubits: tuple[int, ...] = (), + logical_fidelity: float | None = None, + exception: Exception | None = None, + diagnostics: dict[str, Any] | None = None, +) -> CapabilityReport: + constraints = [ + "normalized_selected_coefficients", + "power_of_two_dimension", + "real_or_complex_supported_pyqpanda3_0_3_5", + ] + if method is not PreparationMethod.AMPLITUDE_ENCODE: + constraints.append("binary_sparse_keys") + return CapabilityReport( + method=method, + compatible=compatible, + reason_code=reason_code, + reason=_REASONS[reason_code], + required_qubits=required_qubits, + ancillas=ancillas, + input_constraints=tuple(constraints), + observed_output_qubits=output_qubits, + exception_type=type(exception).__name__ if exception is not None else None, + exception_message=str(exception) if exception is not None else None, + diagnostics={} if diagnostics is None else diagnostics, + failure_stage=failure_stage, + logical_fidelity=logical_fidelity, + ) + + +def _valid_sparse_keys(prepared_input: PreparationInput) -> bool: + width = prepared_input.logical_output_qubits + keys = [key for key, _ in prepared_input.sparse_items] + return bool(keys) and len(keys) == len(set(keys)) and all( + isinstance(key, str) + and len(key) == width + and set(key) <= {"0", "1"} + for key in keys + ) + + +def static_capability_check( + method: PreparationMethod, + prepared_input: PreparationInput, + *, + available_qubits: int | None = None, +) -> CapabilityReport: + """Check deterministic method/input constraints before touching PyQPanda.""" + + required = required_qubit_contract( + method, prepared_input.logical_output_qubits + ) + if method is not PreparationMethod.AMPLITUDE_ENCODE and not _valid_sparse_keys( + prepared_input + ): + return _report( + method, + compatible=False, + reason_code="invalid_sparse_keys", + failure_stage="static", + required_qubits=required, + ) + if available_qubits is not None and available_qubits < required: + return _report( + method, + compatible=False, + reason_code="insufficient_qubits", + failure_stage="static", + required_qubits=required, + diagnostics={"available_qubits": available_qubits}, + ) + return _report( + method, + compatible=True, + reason_code="compatible", + failure_stage=None, + required_qubits=required, + diagnostics={"stage": "static_only"}, + ) + + +def _metadata_valid(build: PreparationBuild) -> bool: + outputs = build.output_qubits + ancillas = build.ancillas + return ( + build.program is not None + and build.circuit is not None + and len(outputs) == build.logical_output_qubits + and len(set(outputs)) == len(outputs) + and len(set(ancillas)) == len(ancillas) + and set(outputs).isdisjoint(ancillas) + and len(outputs) + len(ancillas) == build.required_qubits + and set(outputs).union(ancillas) == set(range(build.required_qubits)) + and ( + build.method is not PreparationMethod.DS_QUANTUM_STATE_PREPARATION + or len(ancillas) == build.logical_output_qubits + ) + and ( + build.method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION + or not ancillas + ) + ) + + +def _logical_state_fidelity( + build: PreparationBuild, + expected: np.ndarray, +) -> float: + qvm = CPUQVM() + qvm.run(build.program, 1) + state = np.asarray(qvm.result().get_state_vector(), dtype=np.complex128) + total_qubits = int(round(math.log2(int(state.size)))) + if 2**total_qubits != state.size or total_qubits != build.required_qubits: + raise RuntimeError("unexpected simulated state width") + + outputs = list(build.output_qubits) + ancillas = [qubit for qubit in range(total_qubits) if qubit not in outputs] + tensor = state.reshape([2] * total_qubits, order="F") + ordered = np.transpose(tensor, outputs + ancillas).reshape( + (2 ** len(outputs), -1), order="F" + ) + reduced = ordered @ ordered.conj().T + fidelity = float(np.real(np.vdot(expected, reduced @ expected))) + if not math.isfinite(fidelity): + raise RuntimeError("non-finite logical fidelity") + return fidelity + + +def assess_capability( + method: PreparationMethod, + prepared_input: PreparationInput, + *, + available_qubits: int | None = None, + verify_logical_state: bool = True, +) -> CapabilityAssessment: + """Run static checks, construction, metadata checks, and optional smoke fidelity.""" + + static = static_capability_check( + method, prepared_input, available_qubits=available_qubits + ) + if not static.compatible: + return CapabilityAssessment(report=static) + + try: + build = build_preparation(method, prepared_input) + except InternalInvariantError: + raise + except (TypeError, ValueError, RuntimeError) as error: + return CapabilityAssessment( + report=_report( + method, + compatible=False, + reason_code="constructor_rejected_input", + failure_stage="construction", + required_qubits=static.required_qubits, + exception=error, + ) + ) + except Exception as error: # backend bindings expose implementation-defined types + return CapabilityAssessment( + report=_report( + method, + compatible=False, + reason_code="unknown_backend_error", + failure_stage="construction", + required_qubits=static.required_qubits, + exception=error, + ) + ) + + if not _metadata_valid(build): + return CapabilityAssessment( + report=_report( + method, + compatible=False, + reason_code="unexpected_output_register", + failure_stage="construction", + required_qubits=build.required_qubits, + ancillas=build.ancillas, + output_qubits=build.output_qubits, + diagnostics=dict(build.diagnostics), + ), + build=build, + ) + + fidelity: float | None = None + if verify_logical_state: + try: + fidelity = _logical_state_fidelity( + build, np.asarray(prepared_input.coefficients, dtype=np.complex128) + ) + except InternalInvariantError: + raise + except Exception as error: + return CapabilityAssessment( + report=_report( + method, + compatible=False, + reason_code="unknown_backend_error", + failure_stage="correctness", + required_qubits=build.required_qubits, + ancillas=build.ancillas, + output_qubits=build.output_qubits, + exception=error, + diagnostics=dict(build.diagnostics), + ), + build=build, + ) + if fidelity < 1.0 - LOGICAL_FIDELITY_TOLERANCE: + return CapabilityAssessment( + report=_report( + method, + compatible=False, + reason_code="logical_fidelity_mismatch", + failure_stage="correctness", + required_qubits=build.required_qubits, + ancillas=build.ancillas, + output_qubits=build.output_qubits, + logical_fidelity=fidelity, + diagnostics=dict(build.diagnostics), + ), + build=build, + ) + + return CapabilityAssessment( + report=_report( + method, + compatible=True, + reason_code="compatible", + failure_stage=None, + required_qubits=build.required_qubits, + ancillas=build.ancillas, + output_qubits=build.output_qubits, + logical_fidelity=fidelity, + diagnostics=dict(build.diagnostics), + ), + build=build, + ) + + +def assess_all_capabilities( + prepared_input: PreparationInput, + *, + methods: tuple[PreparationMethod, ...] = DEFAULT_METHODS, + available_qubits: int | None = None, + verify_logical_state: bool = True, +) -> tuple[CapabilityAssessment, ...]: + """Preserve every candidate report, including incompatible methods.""" + + return tuple( + assess_capability( + method, + prepared_input, + available_qubits=available_qubits, + verify_logical_state=verify_logical_state, + ) + for method in methods + ) + + +__all__ = [ + "LOGICAL_FIDELITY_TOLERANCE", + "CAPABILITY_REASON_CODES", + "CapabilityAssessment", + "static_capability_check", + "assess_capability", + "assess_all_capabilities", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_compiler.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_compiler.py new file mode 100644 index 00000000..ada20586 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_compiler.py @@ -0,0 +1,271 @@ +"""Frozen PyQPanda3 compilation profile and per-attempt records.""" + +from __future__ import annotations + +from collections import Counter +from dataclasses import dataclass +import hashlib +import json +import re +from typing import Any, Callable, Mapping + +from pyqpanda3.core import H, QCircuit, QProg +from pyqpanda3.transpilation import Transpiler + +from pyqpanda_alg.plugin import QFT + +from .config import CompilerConfig, PreparationMethod +from .exceptions import ConfigurationError, InternalInvariantError +from ._preparation import PreparationBuild + + +FROZEN_COMPILER_PROFILE = CompilerConfig() +TECHNICAL_REPETITIONS = 5 +_PROFILE_DOMAIN = b"qseencode-compiler-profile-v1\0" +_IGNORED_IR_TOKENS = { + "QINIT", "CREG", "DAGGER", "ENDDAGGER", "CONTROL", "ENDCONTROL", + "MEASURE", "RESET", "BARRIER", +} + + +@dataclass(frozen=True, slots=True) +class EndToEndProgram: + program: QProg + basis: str + method: PreparationMethod + output_qubits: tuple[int, ...] + ancillas: tuple[int, ...] + required_qubits: int + allocated_qubits: int + + +@dataclass(frozen=True, slots=True) +class CompilationAttempt: + attempt_index: int + success: bool + status: str + compiled_program: QProg | None + compiled_originir: str | None + originir_sha256: str | None + compiled_depth: int | None + compiled_total_gates: int | None + compiled_one_qubit_gates: int | None + compiled_two_qubit_gates: int | None + compiled_cnot_gates: int | None + required_qubits: int + allocated_qubits: int + ancilla_count: int + compiler_profile_fingerprint: str + compiler_profile: Mapping[str, Any] + exception_type: str | None = None + exception_message: str | None = None + diagnostics: Mapping[str, Any] | None = None + + +def compiler_profile_payload(profile: CompilerConfig) -> dict[str, Any]: + return { + "pyqpanda_version": profile.pyqpanda_version, + "topology": profile.topology, + "physical_capacity_multiplier": profile.physical_capacity_multiplier, + "initial_mapping": profile.initial_mapping, + "optimization_level": profile.optimization_level, + "basis_gates": list(profile.basis_gates), + "technical_repetitions": profile.technical_repetitions, + "resource_aggregation": profile.resource_aggregation, + } + + +def compiler_profile_fingerprint(profile: CompilerConfig) -> str: + encoded = json.dumps( + compiler_profile_payload(profile), sort_keys=True, separators=(",", ":") + ).encode("utf-8") + return hashlib.sha256(_PROFILE_DOMAIN + encoded).hexdigest() + + +def linear_topology(width: int) -> list[list[int]]: + if type(width) is not int or width < 2: + raise InternalInvariantError(code="invalid_allocated_width") + return [[index, index + 1] for index in range(width - 1)] + + +def compose_end_to_end_program( + build: PreparationBuild, + *, + basis: str, + profile: CompilerConfig = FROZEN_COMPILER_PROFILE, +) -> EndToEndProgram: + """Place every method on the same 2n hardware and append basis decoding.""" + + if basis not in {"walsh", "fourier"}: + raise ConfigurationError(code="invalid_basis") + logical = build.logical_output_qubits + allocated = profile.physical_capacity_multiplier * logical + if allocated != 2 * logical: + raise ConfigurationError(code="unsupported_physical_capacity_profile") + if build.required_qubits > allocated: + raise InternalInvariantError(code="preparation_exceeds_allocated_width") + outputs = tuple(range(allocated - logical, allocated)) + + circuit = QCircuit() + circuit << build.circuit + if build.method is not PreparationMethod.DS_QUANTUM_STATE_PREPARATION: + # The list overload is old-index -> new-index and leaves the Phase 3 + # build untouched because the circuit above is a fresh container. + circuit.remap(list(outputs)) + elif build.output_qubits != outputs: + raise InternalInvariantError(code="ds_output_register_contract_changed") + + program = QProg(allocated) + program << circuit + if basis == "walsh": + program << [H(qubit) for qubit in outputs] + else: + program << QFT(list(outputs)) + return EndToEndProgram( + program=program, + basis=basis, + method=build.method, + output_qubits=outputs, + ancillas=build.ancillas, + required_qubits=build.required_qubits, + allocated_qubits=allocated, + ) + + +def _parse_operations(originir: str) -> list[tuple[str, tuple[int, ...]]]: + operations: list[tuple[str, tuple[int, ...]]] = [] + for raw in originir.splitlines(): + line = raw.strip() + if not line: + continue + gate = line.split(maxsplit=1)[0].upper() + if gate in _IGNORED_IR_TOKENS: + continue + qubits = tuple(int(value) for value in re.findall(r"q\[(\d+)\]", line)) + if qubits: + operations.append((gate, qubits)) + return operations + + +def _successful_attempt( + compilation: EndToEndProgram, + attempt_index: int, + profile: CompilerConfig, + compiled: QProg, +) -> CompilationAttempt: + originir = compiled.originir() + if not isinstance(originir, str) or not originir.strip(): + raise InternalInvariantError(code="empty_compiled_representation") + operations = _parse_operations(originir) + counts = Counter(gate for gate, _ in operations) + invalid_basis = sorted(set(counts) - set(profile.basis_gates)) + topology = {tuple(edge) for edge in linear_topology(compilation.allocated_qubits)} + topology |= {(right, left) for left, right in topology} + topology_violations = [ + qubits for _, qubits in operations + if len(qubits) == 2 and tuple(qubits) not in topology + ] + one_qubit = sum(len(qubits) == 1 for _, qubits in operations) + two_qubit = sum(len(qubits) == 2 for _, qubits in operations) + cnot = counts.get("CNOT", 0) + total = len(operations) + depth = int(compiled.depth()) + if invalid_basis or topology_violations or min(one_qubit, two_qubit, total, depth) < 0: + raise InternalInvariantError(code="compiled_resource_validation_failed") + if two_qubit != cnot: + raise InternalInvariantError(code="compiled_two_qubit_cnot_mismatch") + payload = compiler_profile_payload(profile) + return CompilationAttempt( + attempt_index=attempt_index, + success=True, + status="success", + compiled_program=compiled, + compiled_originir=originir, + originir_sha256=hashlib.sha256(originir.encode("utf-8")).hexdigest(), + compiled_depth=depth, + compiled_total_gates=total, + compiled_one_qubit_gates=one_qubit, + compiled_two_qubit_gates=two_qubit, + compiled_cnot_gates=cnot, + required_qubits=compilation.required_qubits, + allocated_qubits=compilation.allocated_qubits, + ancilla_count=len(compilation.ancillas), + compiler_profile_fingerprint=compiler_profile_fingerprint(profile), + compiler_profile=payload, + diagnostics={"basis_valid": True, "topology_valid": True}, + ) + + +def compile_attempt( + compilation: EndToEndProgram, + attempt_index: int, + *, + profile: CompilerConfig = FROZEN_COMPILER_PROFILE, + transpiler_factory: Callable[[], Any] = Transpiler, +) -> CompilationAttempt: + if type(attempt_index) is not int or not 0 <= attempt_index < TECHNICAL_REPETITIONS: + raise InternalInvariantError(code="invalid_compilation_attempt_index") + payload = compiler_profile_payload(profile) + fingerprint = compiler_profile_fingerprint(profile) + try: + compiled = transpiler_factory().transpile( + compilation.program, + linear_topology(compilation.allocated_qubits), + {qubit: qubit for qubit in range(compilation.allocated_qubits)}, + profile.optimization_level, + list(profile.basis_gates), + ) + if compiled is None: + raise InternalInvariantError(code="empty_compiled_program") + return _successful_attempt(compilation, attempt_index, profile, compiled) + except InternalInvariantError: + raise + except Exception as error: + return CompilationAttempt( + attempt_index=attempt_index, + success=False, + status="compile_failure", + compiled_program=None, + compiled_originir=None, + originir_sha256=None, + compiled_depth=None, + compiled_total_gates=None, + compiled_one_qubit_gates=None, + compiled_two_qubit_gates=None, + compiled_cnot_gates=None, + required_qubits=compilation.required_qubits, + allocated_qubits=compilation.allocated_qubits, + ancilla_count=len(compilation.ancillas), + compiler_profile_fingerprint=fingerprint, + compiler_profile=payload, + exception_type=type(error).__name__, + exception_message=str(error), + diagnostics={"basis_valid": False, "topology_valid": False}, + ) + + +def compile_five_repetitions( + compilation: EndToEndProgram, + *, + profile: CompilerConfig = FROZEN_COMPILER_PROFILE, + transpiler_factory: Callable[[], Any] = Transpiler, +) -> tuple[CompilationAttempt, ...]: + if profile.technical_repetitions != TECHNICAL_REPETITIONS: + raise ConfigurationError(code="technical_repetitions_must_equal_five") + return tuple( + compile_attempt( + compilation, + attempt_index, + profile=profile, + transpiler_factory=transpiler_factory, + ) + for attempt_index in range(TECHNICAL_REPETITIONS) + ) + + +__all__ = [ + "FROZEN_COMPILER_PROFILE", "TECHNICAL_REPETITIONS", "EndToEndProgram", + "CompilationAttempt", "compiler_profile_payload", "compiler_profile_fingerprint", + "linear_topology", "compose_end_to_end_program", "compile_attempt", + "compile_five_repetitions", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_error_budget.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_error_budget.py new file mode 100644 index 00000000..89d6d64f --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_error_budget.py @@ -0,0 +1,151 @@ +"""Pure ranking and error-budget core for QSEncode-Insight.""" + +from __future__ import annotations + +import math + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +from .exceptions import InputValidationError +from .models import ErrorBudgetResult + + +RANKING_POLICY = "frozen_stable_v1" +MINIMALITY_TOLERANCE = 1e-12 + + +def _coefficient_array(values: ArrayLike) -> NDArray[np.float64] | NDArray[np.complex128]: + try: + source = np.asarray(values) + dtype = np.complex128 if np.iscomplexobj(source) else np.float64 + coefficients = np.array(source, dtype=dtype, order="C", copy=True) + except (TypeError, ValueError, OverflowError) as error: + raise InputValidationError(code="coefficients_not_numeric") from error + if coefficients.ndim != 1 or coefficients.size == 0: + raise InputValidationError(code="invalid_coefficient_dimension") + if not np.all(np.isfinite(coefficients)): + raise InputValidationError(code="nonfinite_coefficients") + return coefficients + + +def stable_magnitude_order(coefficients: ArrayLike) -> NDArray[np.intp]: + """Return the exact Frozen ascending stable magnitude ordering.""" + + values = _coefficient_array(coefficients) + return np.argsort(np.abs(values), kind="stable") + + +def _validate_k(k: int, size: int) -> None: + if type(k) is not int or not 1 <= k <= size: + raise InputValidationError(code="invalid_k") + + +def top_k_indices(coefficients: ArrayLike, k: int) -> NDArray[np.intp]: + values = _coefficient_array(coefficients) + _validate_k(k, int(values.size)) + order = np.argsort(np.abs(values), kind="stable") + return np.asarray(order[-k:], dtype=np.intp) + + +def top_k_coefficients( + coefficients: ArrayLike, + k: int, + *, + normalize: bool = False, +) -> NDArray[np.float64] | NDArray[np.complex128]: + values = _coefficient_array(coefficients) + indices = top_k_indices(values, k) + selected = np.zeros_like(values) + selected[indices] = values[indices] + if normalize: + norm = float(np.linalg.norm(selected)) + if not math.isfinite(norm) or norm <= 0.0: + raise InputValidationError(code="zero_selected_norm") + selected = selected / norm + return selected + + +def _energy_components( + coefficients: ArrayLike, +) -> tuple[NDArray[np.float64] | NDArray[np.complex128], NDArray[np.float64], float, NDArray[np.intp]]: + values = _coefficient_array(coefficients) + energy = np.asarray(np.abs(values) ** 2, dtype=np.float64) + total = float(np.sum(energy, dtype=np.float64)) + if not math.isfinite(total): + raise InputValidationError(code="nonfinite_coefficient_energy") + if total <= 0.0: + raise InputValidationError(code="zero_coefficient_energy") + order = np.argsort(np.abs(values), kind="stable") + return values, energy, total, order + + +def retained_energy_ratio(coefficients: ArrayLike, k: int) -> float: + values, energy, total, order = _energy_components(coefficients) + _validate_k(k, int(values.size)) + return float(np.sum(energy[order[-k:]], dtype=np.float64) / total) + + +def candidate_neighborhood(k_star: int, size: int) -> tuple[int, ...]: + if type(size) is not int or size < 2: + raise InputValidationError(code="invalid_candidate_dimension") + if type(k_star) is not int or not 1 <= k_star <= size: + raise InputValidationError(code="invalid_k_star") + return tuple( + candidate + for candidate in (k_star - 1, k_star, k_star + 1) + if 1 <= candidate <= size - 1 + ) + + +def find_k_star( + coefficients: ArrayLike, + fidelity_target: float, +) -> ErrorBudgetResult: + """Return the minimal retained-energy k and its frozen neighborhood.""" + + if ( + not isinstance(fidelity_target, (float, int)) + or isinstance(fidelity_target, bool) + or not math.isfinite(float(fidelity_target)) + or not 0.0 < float(fidelity_target) <= 1.0 + ): + raise InputValidationError(code="invalid_fidelity_target") + target = float(fidelity_target) + values, energy, total, order = _energy_components(coefficients) + + previous = 0.0 + retained = 0.0 + k_star = int(values.size) + for k in range(1, int(values.size) + 1): + retained = float(np.sum(energy[order[-k:]], dtype=np.float64) / total) + if retained >= target - MINIMALITY_TOLERANCE: + k_star = k + break + previous = retained + + minimality_pass = ( + previous < target + MINIMALITY_TOLERANCE + and retained >= target - MINIMALITY_TOLERANCE + ) + return ErrorBudgetResult( + fidelity_target=target, + k_star=k_star, + retained_energy=retained, + previous_retained_energy=previous, + candidate_k=candidate_neighborhood(k_star, int(values.size)), + minimality_pass=minimality_pass, + ranking_policy=RANKING_POLICY, + ) + + +__all__ = [ + "RANKING_POLICY", + "MINIMALITY_TOLERANCE", + "stable_magnitude_order", + "top_k_indices", + "top_k_coefficients", + "retained_energy_ratio", + "candidate_neighborhood", + "find_k_star", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_preparation.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_preparation.py new file mode 100644 index 00000000..9874805d --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_preparation.py @@ -0,0 +1,237 @@ +"""Private state-preparation adapters for QSEncode-Insight Phase 3. + +The adapters consume an already selected and normalized coefficient vector. +They never rank coefficients, choose ``k``, transpile, or inspect resources. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import math +from typing import Any, Mapping + +import numpy as np +from numpy.typing import ArrayLike, NDArray +from pyqpanda3.core import Encode, QCircuit, QProg + +from .config import PreparationMethod +from .exceptions import InputValidationError, InternalInvariantError + + +# Frozen provenance: final_technical_hardening.py, SHA-256 +# B24336428BD62DFF3C479B5C1207329BC61AA68B92009E77A2EE6537981D11A8. +# This is representation cleanup after Phase 2's exact top-k mask; it does not +# participate in retained-energy or k-star selection. +SPARSE_SUPPORT_THRESHOLD = 1e-14 +STATE_NORM_ATOL = 1e-12 + + +Scalar = float | complex + + +@dataclass(frozen=True, slots=True) +class PreparationInput: + """Normalized selected coefficients plus deterministic API representations.""" + + coefficients: NDArray[np.float64] | NDArray[np.complex128] + logical_dimension: int + logical_output_qubits: int + dense_data: tuple[Scalar, ...] + sparse_items: tuple[tuple[str, Scalar], ...] + support_indices: tuple[int, ...] + support_threshold: float + is_complex: bool + + def sparse_data(self) -> dict[str, Scalar]: + """Return a fresh mutable mapping for the PyQPanda binding.""" + + return dict(self.sparse_items) + + +@dataclass(frozen=True, slots=True) +class PreparationBuild: + """Internal construction result, not the public PreparationArtifact.""" + + method: PreparationMethod + program: QProg + circuit: QCircuit + output_qubits: tuple[int, ...] + ancillas: tuple[int, ...] + required_qubits: int + logical_output_qubits: int + input_representation: str + status: str + diagnostics: Mapping[str, Any] + + +def _is_power_of_two(value: int) -> bool: + return value >= 2 and value & (value - 1) == 0 + + +def _python_scalar(value: np.generic) -> Scalar: + converted = value.item() + return complex(converted) if isinstance(converted, complex) else float(converted) + + +def adapt_preparation_input(coefficients: ArrayLike) -> PreparationInput: + """Validate and adapt an already normalized selected state. + + Complex values are preserved. The caller-owned object is never modified. + """ + + try: + source = np.asarray(coefficients) + dtype = np.complex128 if np.iscomplexobj(source) else np.float64 + values = np.array(source, dtype=dtype, order="C", copy=True) + except (TypeError, ValueError, OverflowError) as error: + raise InputValidationError(code="state_not_numeric") from error + if values.ndim != 1 or not _is_power_of_two(int(values.size)): + raise InputValidationError(code="invalid_state_dimension") + if not np.all(np.isfinite(values)): + raise InputValidationError(code="nonfinite_state") + + norm = float(np.linalg.norm(values)) + if not math.isfinite(norm) or abs(norm - 1.0) > STATE_NORM_ATOL: + raise InputValidationError(code="state_not_normalized") + + logical_dimension = int(values.size) + logical_qubits = logical_dimension.bit_length() - 1 + support = tuple( + int(index) + for index in np.flatnonzero(np.abs(values) > SPARSE_SUPPORT_THRESHOLD) + ) + if not support: + raise InternalInvariantError(code="empty_preparation_support") + + dense = tuple(_python_scalar(value) for value in values) + sparse = tuple( + (format(index, f"0{logical_qubits}b"), _python_scalar(values[index])) + for index in support + ) + values.setflags(write=False) + return PreparationInput( + coefficients=values, + logical_dimension=logical_dimension, + logical_output_qubits=logical_qubits, + dense_data=dense, + sparse_items=sparse, + support_indices=support, + support_threshold=SPARSE_SUPPORT_THRESHOLD, + is_complex=bool(np.iscomplexobj(values)), + ) + + +def required_qubit_contract(method: PreparationMethod, logical_qubits: int) -> int: + """Return the PyQPanda 0.3.5/Frozen v1 allocation contract.""" + + if method in { + PreparationMethod.AMPLITUDE_ENCODE, + PreparationMethod.SPARSE_ISOMETRY, + }: + return logical_qubits + if method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION: + return 2 * logical_qubits + raise InternalInvariantError(code="unsupported_preparation_method") + + +def _normalize_backend_output_register( + method: PreparationMethod, + allocated_qubits: tuple[int, ...], + reported: tuple[int, ...], + logical_qubits: int, + *, + is_complex: bool, +) -> tuple[tuple[int, ...], str | None]: + if len(reported) == logical_qubits and len(set(reported)) == logical_qubits: + return reported, None + + # PyQPanda3 0.3.5's complex amplitude overload reports the same output + # register twice. The circuit itself acts on exactly the supplied n-qubit + # register. This normalization is deliberately narrow and is retained in + # diagnostics; any other malformed register remains a capability failure. + if ( + method is PreparationMethod.AMPLITUDE_ENCODE + and is_complex + and reported == allocated_qubits + allocated_qubits + ): + return allocated_qubits, "deduplicated_exact_repetition" + return reported, None + + +def build_preparation( + method: PreparationMethod, + prepared_input: PreparationInput, +) -> PreparationBuild: + """Invoke exactly one supported PyQPanda state-preparation constructor.""" + + if not isinstance(method, PreparationMethod): + raise InternalInvariantError(code="unsupported_preparation_method") + if not isinstance(prepared_input, PreparationInput): + raise InternalInvariantError(code="invalid_preparation_input") + + required = required_qubit_contract( + method, prepared_input.logical_output_qubits + ) + allocated = tuple(range(required)) + encoder = Encode() + + if method is PreparationMethod.AMPLITUDE_ENCODE: + representation = "dense_list" + return_value = encoder.amplitude_encode(list(allocated), list(prepared_input.dense_data)) + elif method is PreparationMethod.SPARSE_ISOMETRY: + representation = "sparse_binary_map" + return_value = encoder.sparse_isometry( + list(allocated), prepared_input.sparse_data() + ) + else: + representation = "sparse_binary_map" + return_value = encoder.ds_quantum_state_preparation( + list(allocated), prepared_input.sparse_data() + ) + + circuit = encoder.get_circuit() + raw_output = tuple(int(qubit) for qubit in encoder.get_out_qubits()) + output, normalization = _normalize_backend_output_register( + method, + allocated, + raw_output, + prepared_input.logical_output_qubits, + is_complex=prepared_input.is_complex, + ) + ancillas = tuple(qubit for qubit in allocated if qubit not in output) + program = QProg(required) + program << circuit + diagnostics: dict[str, Any] = { + "backend": "pyqpanda3.Encode", + "backend_version": "0.3.5", + "backend_return_value": repr(return_value), + "backend_reported_output_qubits": raw_output, + "allocated_qubits": allocated, + "support_size": len(prepared_input.support_indices), + } + if normalization is not None: + diagnostics["output_register_normalization"] = normalization + + return PreparationBuild( + method=method, + program=program, + circuit=circuit, + output_qubits=output, + ancillas=ancillas, + required_qubits=required, + logical_output_qubits=prepared_input.logical_output_qubits, + input_representation=representation, + status="success", + diagnostics=diagnostics, + ) + + +__all__ = [ + "SPARSE_SUPPORT_THRESHOLD", + "STATE_NORM_ATOL", + "PreparationInput", + "PreparationBuild", + "adapt_preparation_input", + "required_qubit_contract", + "build_preparation", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_resources.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_resources.py new file mode 100644 index 00000000..19d3cbe5 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_resources.py @@ -0,0 +1,111 @@ +"""Five-repeat compiled-resource auditing without selection logic.""" + +from __future__ import annotations + +import statistics +from typing import Any, Callable + +from pyqpanda3.transpilation import Transpiler + +from .config import CompilerConfig +from .exceptions import InternalInvariantError +from .models import ResourceAudit +from ._compiler import ( + FROZEN_COMPILER_PROFILE, + TECHNICAL_REPETITIONS, + CompilationAttempt, + compile_five_repetitions, + compose_end_to_end_program, +) +from ._preparation import PreparationBuild + + +def _median(values: list[int]) -> float: + return float(statistics.median(values)) + + +def aggregate_resource_audit( + build: PreparationBuild, + attempts: tuple[CompilationAttempt, ...], +) -> ResourceAudit: + if len(attempts) != TECHNICAL_REPETITIONS: + raise InternalInvariantError(code="resource_audit_requires_five_attempts") + if tuple(attempt.attempt_index for attempt in attempts) != tuple(range(5)): + raise InternalInvariantError(code="resource_attempt_index_mismatch") + if len({attempt.compiler_profile_fingerprint for attempt in attempts}) != 1: + raise InternalInvariantError(code="mixed_compiler_profiles") + successes = [attempt for attempt in attempts if attempt.success] + common = dict( + required_qubits=build.required_qubits, + allocated_qubits=attempts[0].allocated_qubits, + repetitions=TECHNICAL_REPETITIONS, + ancillas=build.ancillas, + successful_attempts=len(successes), + failed_attempts=TECHNICAL_REPETITIONS - len(successes), + compiler_profile=attempts[0].compiler_profile, + compiler_profile_fingerprint=attempts[0].compiler_profile_fingerprint, + compilation_attempts=attempts, + ) + if len(successes) != TECHNICAL_REPETITIONS: + return ResourceAudit( + compiled_two_qubit_gates=None, + compiled_depth=None, + compiled_total_gates=None, + valid=False, + status="compile_failure", + failure_reason="one_or_more_technical_repetitions_failed", + **common, + ) + + def values(field: str) -> list[int]: + result = [getattr(attempt, field) for attempt in successes] + if any(value is None for value in result): + raise InternalInvariantError(code="missing_success_resource_metric") + return [int(value) for value in result] + + twoq = values("compiled_two_qubit_gates") + depth = values("compiled_depth") + total = values("compiled_total_gates") + oneq = values("compiled_one_qubit_gates") + cnot = values("compiled_cnot_gates") + twoq_median = _median(twoq) + depth_median = _median(depth) + if twoq != cnot: + raise InternalInvariantError(code="compiled_two_qubit_cnot_series_mismatch") + return ResourceAudit( + compiled_two_qubit_gates=twoq_median, + compiled_depth=depth_median, + compiled_total_gates=_median(total), + compiled_one_qubit_gates=_median(oneq), + compiled_cnot_gates=_median(cnot), + two_qubit_range=(float(min(twoq)), float(max(twoq))), + depth_range=(float(min(depth)), float(max(depth))), + total_gate_range=(float(min(total)), float(max(total))), + one_qubit_range=(float(min(oneq)), float(max(oneq))), + q_required_times_depth=build.required_qubits * depth_median, + q_allocated_times_depth=attempts[0].allocated_qubits * depth_median, + valid=True, + status="valid", + failure_reason=None, + **common, + ) + + +def audit_build_resources( + build: PreparationBuild, + *, + basis: str, + profile: CompilerConfig = FROZEN_COMPILER_PROFILE, + transpiler_factory: Callable[[], Any] = Transpiler, + attempts: tuple[CompilationAttempt, ...] | None = None, +) -> ResourceAudit: + compilation = compose_end_to_end_program(build, basis=basis, profile=profile) + actual_attempts = attempts + if actual_attempts is None: + actual_attempts = compile_five_repetitions( + compilation, profile=profile, transpiler_factory=transpiler_factory + ) + return aggregate_resource_audit(build, actual_attempts) + + +__all__ = ["aggregate_resource_audit", "audit_build_resources"] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_selection.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_selection.py new file mode 100644 index 00000000..ddeda96d --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_selection.py @@ -0,0 +1,288 @@ +"""Frozen six-key resource selection, refusal, and attribution.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, Sequence + +import numpy as np +from numpy.typing import ArrayLike + +from .config import ( + DEFAULT_METHODS, + CompilerConfig, + PreparationMethod, + SelectionDecision, +) +from .exceptions import BaselineConstructionError, InternalInvariantError +from .models import ( + AttributionReport, + CandidateResult, + ErrorBudgetResult, + SelectionResult, +) +from ._capability import assess_capability +from ._compiler import FROZEN_COMPILER_PROFILE +from ._error_budget import MINIMALITY_TOLERANCE, retained_energy_ratio, top_k_coefficients +from ._preparation import adapt_preparation_input +from ._resources import audit_build_resources + + +METHOD_ORDER = {method: index for index, method in enumerate(DEFAULT_METHODS)} + + +@dataclass(frozen=True, slots=True) +class CandidateGrid: + dense_full: CandidateResult + candidates: tuple[CandidateResult, ...] + + +@dataclass(frozen=True, slots=True) +class ResourceSelectionRun: + grid: CandidateGrid + selection: SelectionResult + attribution: AttributionReport | None + + +def _candidate_from_state( + coefficients: np.ndarray, + *, + basis: str, + method: PreparationMethod, + k: int, + role: str, + retained_fidelity: float, + fidelity_target: float, + profile: CompilerConfig, +) -> CandidateResult: + prepared = adapt_preparation_input(coefficients) + allocated = profile.physical_capacity_multiplier * prepared.logical_output_qubits + capability = assess_capability( + method, prepared, available_qubits=allocated + ) + audit = None + if capability.report.compatible and capability.build is not None: + audit = audit_build_resources( + capability.build, basis=basis, profile=profile + ) + + if role == "dense_full": + eligible = bool(capability.report.compatible and audit is not None and audit.valid) + reason = "mandatory_baseline" if eligible else "baseline_unavailable" + status = "baseline_valid" if eligible else "baseline_failure" + elif retained_fidelity < fidelity_target - MINIMALITY_TOLERANCE: + eligible, reason, status = False, "fidelity_below_target", "fidelity_ineligible" + elif not capability.report.compatible: + eligible, reason, status = False, "capability_incompatible", "capability_incompatible" + elif audit is None or not audit.valid: + eligible, reason, status = False, "resource_audit_invalid", "resource_invalid" + else: + eligible, reason, status = True, "eligible", "eligible" + + return CandidateResult( + candidate_id=( + "dense_full__amplitude_encode" + if role == "dense_full" + else f"compressed__k{k}__{method.value}" + ), + method=method, + k=k, + status=status, + verified_fidelity=capability.report.logical_fidelity, + resource_audit=audit, + failure_reason=None if eligible else reason, + capability=capability.report, + retained_fidelity=float(retained_fidelity), + eligible=eligible, + eligibility_reason=reason, + role=role, + ) + + +def generate_candidate_grid( + coefficients: ArrayLike, + *, + basis: str, + error_budget: ErrorBudgetResult, + profile: CompilerConfig = FROZEN_COMPILER_PROFILE, + methods: Sequence[PreparationMethod] = DEFAULT_METHODS, +) -> CandidateGrid: + values = np.asarray(coefficients) + dense_full = _candidate_from_state( + values, + basis=basis, + method=PreparationMethod.AMPLITUDE_ENCODE, + k=int(values.size), + role="dense_full", + retained_fidelity=1.0, + fidelity_target=error_budget.fidelity_target, + profile=profile, + ) + candidates: list[CandidateResult] = [] + for k in error_budget.candidate_k: + retained = retained_energy_ratio(values, k) + selected = top_k_coefficients(values, k, normalize=True) + for method in methods: + candidates.append( + _candidate_from_state( + selected, + basis=basis, + method=method, + k=k, + role=("dense_compressed" if method is PreparationMethod.AMPLITUDE_ENCODE else "method_candidate"), + retained_fidelity=retained, + fidelity_target=error_budget.fidelity_target, + profile=profile, + ) + ) + return CandidateGrid(dense_full=dense_full, candidates=tuple(candidates)) + + +def selector_key(candidate: CandidateResult) -> tuple[float, float, int, int, int, int]: + audit = candidate.resource_audit + if not candidate.eligible or audit is None or not audit.valid: + raise InternalInvariantError(code="selector_received_ineligible_candidate") + if audit.compiled_two_qubit_gates is None or audit.compiled_depth is None: + raise InternalInvariantError(code="selector_missing_primary_resource") + return ( + audit.compiled_two_qubit_gates, + audit.compiled_depth, + audit.required_qubits, + len(audit.ancillas), + candidate.k, + METHOD_ORDER[candidate.method], + ) + + +def select_best_eligible(candidates: Iterable[CandidateResult]) -> CandidateResult | None: + eligible = [ + candidate for candidate in candidates + if candidate.eligible + and candidate.resource_audit is not None + and candidate.resource_audit.valid + ] + return min(eligible, key=selector_key) if eligible else None + + +def select_resource_candidate( + baseline: CandidateResult, + candidates: Iterable[CandidateResult], +) -> SelectionResult: + baseline_audit = baseline.resource_audit + if ( + baseline_audit is None + or not baseline_audit.valid + or baseline_audit.compiled_two_qubit_gates is None + or baseline_audit.compiled_depth is None + ): + raise BaselineConstructionError(code="dense_full_resource_invalid") + best = select_best_eligible(candidates) + if best is None: + return SelectionResult( + decision=SelectionDecision.DO_NOT_COMPRESS, + reason_code="no_eligible_compressed_candidate", + reason="No compressed candidate satisfied fidelity, capability, and resource validity.", + baseline_resource=baseline_audit, + ) + best_audit = best.resource_audit + if best_audit is None or best_audit.compiled_two_qubit_gates is None or best_audit.compiled_depth is None: + raise InternalInvariantError(code="best_candidate_missing_resource") + delta_twoq = baseline_audit.compiled_two_qubit_gates - best_audit.compiled_two_qubit_gates + delta_depth = baseline_audit.compiled_depth - best_audit.compiled_depth + improves = delta_twoq > 0 or delta_depth > 0 + return SelectionResult( + decision=(SelectionDecision.COMPRESS if improves else SelectionDecision.DO_NOT_COMPRESS), + reason_code=( + "compressed_candidate_improves_primary_resource" + if improves else "no_primary_resource_improvement" + ), + reason=( + "At least one primary compiled resource strictly improves." + if improves else "Neither compiled two-qubit gates nor depth improves." + ), + selected_candidate_id=best.candidate_id if improves else None, + method=best.method if improves else None, + k=best.k if improves else None, + baseline_resource=baseline_audit, + best_compressed_candidate_id=best.candidate_id, + best_compressed_resource=best_audit, + comparison_metrics={ + "two_qubit_difference": delta_twoq, + "depth_difference": delta_depth, + }, + ) + + +def _valid_primary(candidate: CandidateResult) -> tuple[float, float]: + audit = candidate.resource_audit + if audit is None or not audit.valid or audit.compiled_two_qubit_gates is None or audit.compiled_depth is None: + raise InternalInvariantError(code="attribution_resource_invalid") + return audit.compiled_two_qubit_gates, audit.compiled_depth + + +def compute_attribution( + dense_full: CandidateResult, + dense_compressed: CandidateResult, + selected: CandidateResult, +) -> AttributionReport: + full_twoq, full_depth = _valid_primary(dense_full) + dense_twoq, dense_depth = _valid_primary(dense_compressed) + selected_twoq, selected_depth = _valid_primary(selected) + total_twoq = full_twoq - selected_twoq + trunc_twoq = full_twoq - dense_twoq + prep_twoq = dense_twoq - selected_twoq + total_depth = full_depth - selected_depth + trunc_depth = full_depth - dense_depth + prep_depth = dense_depth - selected_depth + return AttributionReport( + truncation_two_qubit_gain=(100.0 * trunc_twoq / full_twoq if full_twoq else None), + preparation_two_qubit_gain=(100.0 * prep_twoq / full_twoq if full_twoq else None), + truncation_depth_gain=(100.0 * trunc_depth / full_depth if full_depth else None), + preparation_depth_gain=(100.0 * prep_depth / full_depth if full_depth else None), + total_two_qubit_difference=total_twoq, + truncation_two_qubit_difference=trunc_twoq, + preparation_two_qubit_difference=prep_twoq, + total_depth_difference=total_depth, + truncation_depth_difference=trunc_depth, + preparation_depth_difference=prep_depth, + two_qubit_identity_error=abs(total_twoq - (trunc_twoq + prep_twoq)), + depth_identity_error=abs(total_depth - (trunc_depth + prep_depth)), + ) + + +def run_resource_selection( + coefficients: ArrayLike, + *, + basis: str, + error_budget: ErrorBudgetResult, + profile: CompilerConfig = FROZEN_COMPILER_PROFILE, + methods: Sequence[PreparationMethod] = DEFAULT_METHODS, +) -> ResourceSelectionRun: + grid = generate_candidate_grid( + coefficients, + basis=basis, + error_budget=error_budget, + profile=profile, + methods=methods, + ) + selection = select_resource_candidate(grid.dense_full, grid.candidates) + attribution = None + if selection.decision is SelectionDecision.COMPRESS: + selected = next( + candidate for candidate in grid.candidates + if candidate.candidate_id == selection.selected_candidate_id + ) + dense = next( + candidate for candidate in grid.candidates + if candidate.k == selected.k + and candidate.method is PreparationMethod.AMPLITUDE_ENCODE + ) + attribution = compute_attribution(grid.dense_full, dense, selected) + return ResourceSelectionRun(grid=grid, selection=selection, attribution=attribution) + + +__all__ = [ + "METHOD_ORDER", "CandidateGrid", "ResourceSelectionRun", "generate_candidate_grid", + "selector_key", "select_best_eligible", "select_resource_candidate", + "compute_attribution", "run_resource_selection", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_transforms.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_transforms.py new file mode 100644 index 00000000..cdea37fc --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_transforms.py @@ -0,0 +1,174 @@ +"""Pure orthonormal transform layer for QSEncode-Insight. + +Walsh production uses an iterative O(N log N) FWHT. The explicit Hadamard +matrix is restricted to small diagnostics and tests. Fourier production uses +SciPy's orthonormal FFT without constructing a quantum circuit. +""" + +from __future__ import annotations + +import math + +import numpy as np +from numpy.typing import ArrayLike, NDArray +from scipy.fft import fft +from scipy.linalg import hadamard + +from .exceptions import InputValidationError, InternalInvariantError +from .models import TransformDiagnostics + + +TRANSFORM_ATOL = 1e-12 +TRANSFORM_RTOL = 1e-10 + + +def _transform_array(values: ArrayLike) -> NDArray[np.float64] | NDArray[np.complex128]: + try: + source = np.asarray(values) + dtype = np.complex128 if np.iscomplexobj(source) else np.float64 + array = np.array(source, dtype=dtype, order="C", copy=True) + except (TypeError, ValueError, OverflowError) as error: + raise InputValidationError(code="transform_not_numeric") from error + if array.ndim != 1: + raise InputValidationError(code="transform_not_one_dimensional") + if not np.all(np.isfinite(array)): + raise InputValidationError(code="transform_nonfinite") + return array + + +def _require_power_of_two_length(array: np.ndarray) -> None: + size = int(array.size) + if size <= 0 or size & (size - 1): + raise InputValidationError(code="transform_non_power_of_two") + + +def _validated_transform_energy(array: np.ndarray) -> float: + with np.errstate(over="ignore", invalid="ignore"): + energy = float(np.sum(np.abs(array) ** 2, dtype=np.float64)) + if not math.isfinite(energy): + raise InternalInvariantError(code="nonfinite_transform_energy") + if energy <= 0.0: + raise InternalInvariantError(code="zero_transform_energy") + return energy + + +def _validate_transform_output( + coefficients: np.ndarray, + *, + input_energy: float, +) -> tuple[float, float]: + if not np.all(np.isfinite(coefficients)): + raise InternalInvariantError(code="nonfinite_transform_coefficients") + output_energy = _validated_transform_energy(coefficients) + parseval_error = abs(output_energy - input_energy) + allowed_error = TRANSFORM_ATOL + TRANSFORM_RTOL * input_energy + if parseval_error > allowed_error: + raise InternalInvariantError(code="parseval_invariant_failed") + return output_energy, parseval_error + + +def normalized_fwht( + values: ArrayLike, +) -> NDArray[np.float64] | NDArray[np.complex128]: + """Return the orthonormal iterative Walsh-Hadamard transform.""" + + result = _transform_array(values) + _require_power_of_two_length(result) + input_energy = _validated_transform_energy(result) + size = int(result.size) + width = 1 + with np.errstate(over="ignore", invalid="ignore"): + while width < size: + block = width * 2 + for start in range(0, size, block): + left = result[start : start + width].copy() + right = result[start + width : start + block].copy() + result[start : start + width] = left + right + result[start + width : start + block] = left - right + width = block + result /= math.sqrt(size) + _validate_transform_output(result, input_energy=input_energy) + return result + + +def explicit_walsh_oracle( + values: ArrayLike, + *, + maximum_size: int = 64, +) -> NDArray[np.float64] | NDArray[np.complex128]: + """Return the O(N^2) explicit Walsh oracle for small diagnostics only.""" + + array = _transform_array(values) + _require_power_of_two_length(array) + input_energy = _validated_transform_energy(array) + if array.size > maximum_size: + raise InputValidationError(code="walsh_oracle_size_exceeded") + with np.errstate(over="ignore", invalid="ignore"): + result = np.asarray(hadamard(array.size) @ array / math.sqrt(array.size)) + _validate_transform_output(result, input_energy=input_energy) + return result + + +def normalized_fourier(values: ArrayLike) -> NDArray[np.complex128]: + """Return SciPy FFT coefficients in orthonormal ordering/convention.""" + + array = _transform_array(values) + _require_power_of_two_length(array) + input_energy = _validated_transform_energy(array) + with np.errstate(over="ignore", invalid="ignore"): + result = np.asarray(fft(array, norm="ortho"), dtype=np.complex128) + _validate_transform_output(result, input_energy=input_energy) + return result + + +def analyze_transform( + amplitudes: ArrayLike, + *, + basis: str, + oracle_check: bool = False, +) -> tuple[NDArray[np.float64] | NDArray[np.complex128], TransformDiagnostics]: + """Run one explicit basis transform and return its pure diagnostics.""" + + source = _transform_array(amplitudes) + _require_power_of_two_length(source) + input_energy = _validated_transform_energy(source) + if basis == "walsh": + coefficients = normalized_fwht(source) + implementation = "iterative_fwht_v1" + oracle_checked = bool(oracle_check) + oracle_error: float | None = None + if oracle_checked: + oracle = explicit_walsh_oracle(source) + oracle_error = float(np.max(np.abs(coefficients - oracle))) + elif basis == "fourier": + coefficients = normalized_fourier(source) + implementation = "scipy_fft_ortho_v1" + oracle_checked = False + oracle_error = None + else: + raise InputValidationError(code="invalid_transform_basis") + + _, parseval_error = _validate_transform_output( + coefficients, + input_energy=input_energy, + ) + diagnostics = TransformDiagnostics( + basis=basis, + coefficient_count=int(coefficients.size), + parseval_error=parseval_error, + implementation=implementation, + normalization="orthonormal", + oracle_checked=oracle_checked, + oracle_max_abs_error=oracle_error, + ) + return coefficients, diagnostics + + +__all__ = [ + "TRANSFORM_ATOL", + "TRANSFORM_RTOL", + "normalized_fwht", + "explicit_walsh_oracle", + "normalized_fourier", + "analyze_transform", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_validation.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_validation.py new file mode 100644 index 00000000..31fb9212 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_validation.py @@ -0,0 +1,153 @@ +"""Pure input canonicalization for QSEncode-Insight. + +This implementation is independent of the legacy QSpare_Code validator and has +no PyQPanda or Frozen-evidence runtime dependency. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import struct + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +from .config import InputPolicy +from .exceptions import InputValidationError +from .models import InputSummary + + +ORIGINAL_INPUT_DOMAIN = b"qseencode-original-input-v1\0" +EFFECTIVE_PROBABILITY_DOMAIN = b"qseencode-effective-probability-v1\0" +NORMALIZED_CHANGE_TOLERANCE = 1e-12 + + +@dataclass(frozen=True, slots=True) +class ValidatedInput: + """Private pure-core output; probabilities are a read-only float64 copy.""" + + summary: InputSummary + probabilities: NDArray[np.float64] + + +def canonical_probability_sha256(vector: ArrayLike, *, domain: bytes) -> str: + """Hash a one-dimensional vector under the frozen binary v1 contract.""" + + if not isinstance(domain, bytes) or not domain: + raise ValueError("domain must be non-empty bytes") + try: + array = np.asarray(vector, dtype=np.float64) + except (TypeError, ValueError, OverflowError) as error: + raise InputValidationError(code="not_numeric") from error + if array.ndim != 1: + raise InputValidationError(code="not_one_dimensional") + + little_endian = np.ascontiguousarray(array, dtype=np.dtype(" bool: + return value > 0 and value & (value - 1) == 0 + + +def _next_power_of_two(value: int) -> int: + return 1 << (value - 1).bit_length() + + +def canonicalize_probabilities( + probabilities: ArrayLike, + *, + policy: InputPolicy | None = None, +) -> ValidatedInput: + """Validate, normalize, pad, and hash a probability vector.""" + + active_policy = policy if policy is not None else InputPolicy() + if not isinstance(active_policy, InputPolicy): + raise InputValidationError(code="invalid_input_policy") + + try: + source = np.asarray(probabilities) + if np.iscomplexobj(source): + raise InputValidationError(code="complex_probability") + converted = np.asarray(source, dtype=np.float64) + except InputValidationError: + raise + except (TypeError, ValueError, OverflowError) as error: + raise InputValidationError(code="not_numeric") from error + if converted.ndim != 1: + raise InputValidationError(code="not_one_dimensional") + if converted.size < 2: + raise InputValidationError(code="insufficient_dimension") + + original = np.array(converted, dtype=np.float64, order="C", copy=True) + if not np.all(np.isfinite(original)): + raise InputValidationError(code="nonfinite_probability") + if np.any(original < 0.0): + raise InputValidationError(code="negative_probability") + + original_sum = float(np.sum(original, dtype=np.float64)) + if not np.isfinite(original_sum): + raise InputValidationError(code="nonfinite_probability") + if original_sum <= 0.0: + raise InputValidationError(code="zero_mass") + + original_hash = canonical_probability_sha256( + original, domain=ORIGINAL_INPUT_DOMAIN + ) + sum_difference = abs(original_sum - 1.0) + if ( + active_policy.normalization == "strict" + and sum_difference > active_policy.normalization_tolerance + ): + raise InputValidationError(code="not_normalized") + + normalized = original / original_sum + original_length = int(normalized.size) + if active_policy.padding == "reject": + if not _is_power_of_two(original_length): + raise InputValidationError(code="non_power_of_two") + padded_length = original_length + else: + padded_length = _next_power_of_two(original_length) + + padding_count = padded_length - original_length + if padding_count: + effective = np.pad( + normalized, + (0, padding_count), + mode="constant", + constant_values=0.0, + ) + else: + effective = np.array(normalized, dtype=np.float64, order="C", copy=True) + effective = np.ascontiguousarray(effective, dtype=np.float64) + + effective_hash = canonical_probability_sha256( + effective, domain=EFFECTIVE_PROBABILITY_DOMAIN + ) + effective.setflags(write=False) + summary = InputSummary( + original_length=original_length, + padded_length=padded_length, + original_sum=original_sum, + normalized=sum_difference > NORMALIZED_CHANGE_TOLERANCE, + padding_count=padding_count, + original_input_sha256=original_hash, + effective_probability_sha256=effective_hash, + ) + return ValidatedInput(summary=summary, probabilities=effective) + + +__all__ = [ + "ValidatedInput", + "ORIGINAL_INPUT_DOMAIN", + "EFFECTIVE_PROBABILITY_DOMAIN", + "canonical_probability_sha256", + "canonicalize_probabilities", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_verification.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_verification.py new file mode 100644 index 00000000..4368aef6 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/_verification.py @@ -0,0 +1,320 @@ +"""Compiled semantic verification for an already frozen resource selection. + +This module migrates the constrained mapping rules validated by Final +Technical Hardening and the Locked Generalization runner. It never compiles, +ranks, selects, or substitutes a candidate. +""" + +from __future__ import annotations + +import itertools +import math +from typing import Any + +import numpy as np +from pyqpanda3.core import CPUQVM, QProg + +from .config import PreparationMethod, VerificationLevel +from .exceptions import InternalInvariantError +from .models import SemanticVerification, SemanticVerificationAttempt +from ._compiler import TECHNICAL_REPETITIONS, CompilationAttempt +from ._compiler import compose_end_to_end_program +from ._error_budget import top_k_coefficients +from ._preparation import adapt_preparation_input, build_preparation +from ._selection import ResourceSelectionRun + + +SEMANTIC_TOLERANCE = 1e-10 +_FIDELITY_TIE_TOLERANCE = 1e-13 + + +def _simulate(program: QProg) -> np.ndarray: + machine = CPUQVM() + machine.run(program, 1) + state = np.asarray(machine.result().get_state_vector(), dtype=np.complex128) + if state.ndim != 1 or state.size < 2 or state.size & (state.size - 1): + raise InternalInvariantError(code="invalid_semantic_statevector") + if not np.all(np.isfinite(state)): + raise InternalInvariantError(code="nonfinite_semantic_statevector") + return state + + +def _reduced_density(state: np.ndarray, ordered_output: tuple[int, ...]) -> np.ndarray: + total = int(round(math.log2(int(state.size)))) + if ( + len(set(ordered_output)) != len(ordered_output) + or any(qubit < 0 or qubit >= total for qubit in ordered_output) + ): + raise InternalInvariantError(code="invalid_semantic_output_register") + ancillas = tuple(qubit for qubit in range(total) if qubit not in ordered_output) + tensor = state.reshape([2] * total, order="F") + reordered = np.transpose(tensor, axes=ordered_output + ancillas) + matrix = reordered.reshape( + (2 ** len(ordered_output), 2 ** len(ancillas)), order="F" + ) + return matrix @ matrix.conj().T + + +def _logical_target( + logical_state: np.ndarray, output_qubits: tuple[int, ...] +) -> tuple[np.ndarray, float]: + density = _reduced_density(logical_state, output_qubits) + values, vectors = np.linalg.eigh(density) + target = vectors[:, int(np.argmax(values))] + purity = float(np.real(np.trace(density @ density))) + return target, purity + + +def _fidelity(target: np.ndarray, state: np.ndarray, output: tuple[int, ...]) -> float: + density = _reduced_density(state, output) + value = float(np.real(np.vdot(target, density @ target))) + if not math.isfinite(value): + raise InternalInvariantError(code="nonfinite_semantic_fidelity") + return min(1.0, max(0.0, value)) + + +def _best_within_register( + target: np.ndarray, state: np.ndarray, output: tuple[int, ...] +) -> dict[str, Any]: + best = -1.0 + mapping: tuple[int, ...] = () + ties = 0 + tested = 0 + for permutation in itertools.permutations(output): + tested += 1 + value = _fidelity(target, state, permutation) + if value > best + _FIDELITY_TIE_TOLERANCE: + best, mapping, ties = value, permutation, 1 + elif abs(value - best) <= _FIDELITY_TIE_TOLERANCE: + ties += 1 + return { + "fidelity": best, + "mapping": mapping, + "mapping_method": "output_register_constrained_permutation", + "mapping_ties": ties, + "mappings_tested": tested, + "pure_subsets": None, + } + + +def _best_ordered_output_subset( + target: np.ndarray, state: np.ndarray, output_size: int +) -> dict[str, Any]: + total = int(round(math.log2(int(state.size)))) + pure_subsets: list[tuple[int, ...]] = [] + for subset in itertools.combinations(range(total), output_size): + density = _reduced_density(state, subset) + purity = float(np.real(np.trace(density @ density))) + if purity >= 1.0 - SEMANTIC_TOLERANCE: + pure_subsets.append(subset) + best = -1.0 + mapping: tuple[int, ...] = () + ties = 0 + tested = 0 + for subset in pure_subsets: + for permutation in itertools.permutations(subset): + tested += 1 + value = _fidelity(target, state, permutation) + if value > best + _FIDELITY_TIE_TOLERANCE: + best, mapping, ties = value, permutation, 1 + elif abs(value - best) <= _FIDELITY_TIE_TOLERANCE: + ties += 1 + return { + "fidelity": best, + "mapping": mapping, + "mapping_method": "ordered_output_subset_with_ancilla_trace", + "mapping_ties": ties, + "mappings_tested": tested, + "pure_subsets": len(pure_subsets), + } + + +def certify_compiled_attempt( + logical_program: QProg, + compiled_program: QProg, + *, + output_qubits: tuple[int, ...], + method: PreparationMethod, + attempt_index: int, +) -> SemanticVerificationAttempt: + """Certify one existing compiled attempt without recompilation.""" + + logical_state = _simulate(logical_program) + compiled_state = _simulate(compiled_program) + target, logical_purity = _logical_target(logical_state, output_qubits) + evidence = _best_within_register(target, compiled_state, output_qubits) + if ( + evidence["fidelity"] < 1.0 - SEMANTIC_TOLERANCE + and method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION + ): + evidence = _best_ordered_output_subset( + target, compiled_state, len(output_qubits) + ) + if logical_purity < 1.0 - SEMANTIC_TOLERANCE: + status = "uncertified" + reason = "logical_output_purity_below_tolerance" + elif evidence["fidelity"] >= 1.0 - SEMANTIC_TOLERANCE: + status = "certified_pass" + reason = "constrained_semantic_fidelity_passed" + else: + status = "certified_fail" + reason = "constrained_semantic_fidelity_below_tolerance" + diagnostics = { + "logical_output_purity": logical_purity, + "mapping_ties": evidence["mapping_ties"], + "mappings_tested": evidence["mappings_tested"], + "pure_subsets": evidence["pure_subsets"], + "reason": reason, + "provenance": "migrated_frozen_constrained_mapping_v1", + } + return SemanticVerificationAttempt( + attempt_index=attempt_index, + status=status, + fidelity=float(evidence["fidelity"]), + mapping_method=str(evidence["mapping_method"]), + output_qubits=output_qubits, + mapping=tuple(int(qubit) for qubit in evidence["mapping"]), + ancilla_treatment="reduced_density_partial_trace", + diagnostics=diagnostics, + ) + + +def standard_verification() -> SemanticVerification: + return SemanticVerification( + level=VerificationLevel.STANDARD, + status="not_run_by_standard", + recommendation_valid=True, + minimum_fidelity=None, + technical_repetitions=0, + attempts=(), + ) + + +def audit_compiled_attempts( + *, + logical_program: QProg, + attempts: tuple[CompilationAttempt, ...], + output_qubits: tuple[int, ...], + method: PreparationMethod, + selected_candidate_id: str, +) -> SemanticVerification: + if len(attempts) != TECHNICAL_REPETITIONS: + raise InternalInvariantError(code="audit_requires_exactly_five_attempts") + if tuple(item.attempt_index for item in attempts) != tuple(range(5)): + raise InternalInvariantError(code="audit_attempt_index_mismatch") + + records: list[SemanticVerificationAttempt] = [] + for attempt in attempts: + if not attempt.success or attempt.compiled_program is None: + records.append( + SemanticVerificationAttempt( + attempt_index=attempt.attempt_index, + status="compile_unavailable", + fidelity=None, + mapping_method=None, + output_qubits=output_qubits, + exception_type=attempt.exception_type, + exception_message=attempt.exception_message, + diagnostics={"compiler_status": attempt.status}, + ) + ) + continue + try: + records.append( + certify_compiled_attempt( + logical_program, + attempt.compiled_program, + output_qubits=output_qubits, + method=method, + attempt_index=attempt.attempt_index, + ) + ) + except InternalInvariantError: + raise + except Exception as error: + records.append( + SemanticVerificationAttempt( + attempt_index=attempt.attempt_index, + status="uncertified", + fidelity=None, + mapping_method=None, + output_qubits=output_qubits, + exception_type=type(error).__name__, + exception_message=str(error), + diagnostics={"reason": "semantic_verifier_exception"}, + ) + ) + + statuses = {record.status for record in records} + if statuses == {"certified_pass"}: + overall = "certified_pass" + elif "certified_fail" in statuses: + overall = "certified_fail" + else: + overall = "uncertified" + fidelities = [record.fidelity for record in records if record.fidelity is not None] + return SemanticVerification( + level=VerificationLevel.AUDIT, + status=overall, + recommendation_valid=overall == "certified_pass", + minimum_fidelity=min(fidelities) if fidelities else None, + technical_repetitions=TECHNICAL_REPETITIONS, + selected_candidate_id=selected_candidate_id, + attempts=tuple(records), + ) + + +def verify_resource_selection( + coefficients: np.ndarray, + *, + basis: str, + run: ResourceSelectionRun, + level: VerificationLevel, +) -> SemanticVerification: + """Verify the already selected candidate using its existing five attempts.""" + + if level is VerificationLevel.STANDARD: + return standard_verification() + if level is not VerificationLevel.AUDIT: + raise InternalInvariantError(code="unsupported_verification_level") + + if run.selection.decision.value == "compress": + candidate = next( + ( + item for item in run.grid.candidates + if item.candidate_id == run.selection.selected_candidate_id + ), + None, + ) + if candidate is None: + raise InternalInvariantError(code="selected_candidate_missing") + selected_coefficients = top_k_coefficients( + coefficients, candidate.k, normalize=True + ) + else: + candidate = run.grid.dense_full + selected_coefficients = np.asarray(coefficients) + + audit = candidate.resource_audit + if audit is None: + raise InternalInvariantError(code="verification_resource_audit_missing") + attempts = tuple(audit.compilation_attempts) + prepared = adapt_preparation_input(selected_coefficients) + build = build_preparation(candidate.method, prepared) + logical = compose_end_to_end_program(build, basis=basis) + return audit_compiled_attempts( + logical_program=logical.program, + attempts=attempts, + output_qubits=logical.output_qubits, + method=candidate.method, + selected_candidate_id=candidate.candidate_id, + ) + + +__all__ = [ + "SEMANTIC_TOLERANCE", + "certify_compiled_attempt", + "standard_verification", + "audit_compiled_attempts", + "verify_resource_selection", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/assets/benchmark_by_dimension.svg b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/assets/benchmark_by_dimension.svg new file mode 100644 index 00000000..3ec1c8c1 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/assets/benchmark_by_dimension.svg @@ -0,0 +1,30 @@ + + Selector-all resource reduction by dimension + Descriptive cell-level medians for Walsh and Fourier, compiled two-qubit gates and depth, from N 8 to 64. + + Selector-all reduction by dimension + Descriptive medians over 60 cells at each basis and N; not the preregistered Gate aggregation + + + + + 0%20%40%60%80%100% + + + N=8N=16N=32N=64 + + + + + + + + + + + Walsh 2q + Walsh depth + Fourier 2q + Fourier depth + + diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/assets/benchmark_family_heatmap.svg b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/assets/benchmark_family_heatmap.svg new file mode 100644 index 00000000..e946417d --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/assets/benchmark_family_heatmap.svg @@ -0,0 +1,28 @@ + + Family-level selector-all resource reductions + Heatmap of preregistered family medians for Walsh and Fourier compiled two-qubit gates and depth. + + Family-level selector-all reductions + Preregistered family medians; Dirichlet is retained as negative evidence + + GaussianBimodalExponentialStepDirichlet + + + Walsh 2qWalsh depthFourier 2qFourier depth + + + + + + + + + 47.85%45.27%93.12%44.56%0.01% + 48.78%49.31%94.46%45.80%0.61% + 90.64%89.24%71.11%39.27%12.28% + 90.42%88.98%69.82%37.09%12.76% + + + 0%100% + + diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/cli.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/cli.py new file mode 100644 index 00000000..7511b78b --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/cli.py @@ -0,0 +1,70 @@ +"""Minimal command-line interface for QSEncode-Insight.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Sequence + +from .exceptions import QSEncodeInsightError +from .insight import QSEncodeInsight + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="python -m pyqpanda_alg.QSEncode.cli", + description="QSEncode-Insight compiled-resource analysis", + ) + subcommands = parser.add_subparsers(dest="command", required=True) + analyze = subcommands.add_parser( + "analyze", help="analyze one probability distribution" + ) + analyze.add_argument("--basis", choices=("walsh", "fourier"), required=True) + analyze.add_argument("--fidelity-target", type=float, default=0.99) + analyze.add_argument( + "--verification", choices=("standard", "audit"), default="standard" + ) + source = analyze.add_mutually_exclusive_group(required=True) + source.add_argument("--input-json", help="inline JSON probability list") + source.add_argument("--input-file", type=Path, help="UTF-8 JSON probability file") + analyze.add_argument("--pretty", action="store_true", help="indent JSON output") + return parser + + +def _probabilities(arguments: argparse.Namespace, parser: argparse.ArgumentParser): + try: + text = ( + arguments.input_json + if arguments.input_json is not None + else arguments.input_file.read_text(encoding="utf-8") + ) + values = json.loads(text) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + parser.error(f"invalid probability input: {error}") + if not isinstance(values, list): + parser.error("invalid probability input: expected a JSON list") + return values + + +def main(argv: Sequence[str] | None = None) -> int: + parser = _parser() + arguments = parser.parse_args(argv) + probabilities = _probabilities(arguments, parser) + try: + result = QSEncodeInsight( + basis=arguments.basis, + fidelity_target=arguments.fidelity_target, + verification=arguments.verification, + ).analyze(probabilities) + except QSEncodeInsightError as error: + parser.error(f"analysis failed [{error.code}]: {error}") + print(result.to_json(indent=2 if arguments.pretty else None)) + return 0 + + +if __name__ == "__main__": # pragma: no cover - exercised by smoke command + raise SystemExit(main()) + + +__all__ = ["main"] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/config.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/config.py new file mode 100644 index 00000000..cac151e1 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/config.py @@ -0,0 +1,187 @@ +"""Frozen Phase 1 configuration and fingerprint contracts. + +This module contains no probability processing, transform, candidate, +transpilation, selection, or verification implementation. +""" + +from __future__ import annotations + +from dataclasses import dataclass, fields, is_dataclass +from enum import Enum +import hashlib +import json +import math +from typing import Any, Mapping + +from .exceptions import ConfigurationError + + +SCHEMA_VERSION = "qseencode-insight-v1" +SELECTION_POLICY = "frozen_lexicographic_v1" +_FINGERPRINT_DOMAIN = b"qseencode-analysis-config-v1\0" + + +class PreparationMethod(str, Enum): + AMPLITUDE_ENCODE = "amplitude_encode" + SPARSE_ISOMETRY = "sparse_isometry" + DS_QUANTUM_STATE_PREPARATION = "ds_quantum_state_preparation" + + +DEFAULT_METHODS = ( + PreparationMethod.AMPLITUDE_ENCODE, + PreparationMethod.SPARSE_ISOMETRY, + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, +) + + +class EvidenceScopeStatus(str, Enum): + VALIDATED_DEFAULT = "validated_default" + OUTSIDE_VALIDATED_SCOPE = "outside_validated_scope" + + +class VerificationLevel(str, Enum): + STANDARD = "standard" + AUDIT = "audit" + + +class SelectionDecision(str, Enum): + COMPRESS = "compress" + DO_NOT_COMPRESS = "do_not_compress" + + +@dataclass(frozen=True, slots=True) +class InputPolicy: + normalization: str = "normalize" + padding: str = "next_power_of_two" + normalization_tolerance: float = 1e-12 + + def __post_init__(self) -> None: + if self.normalization not in {"normalize", "strict"}: + raise ConfigurationError(code="invalid_normalization_policy") + if self.padding not in {"next_power_of_two", "reject"}: + raise ConfigurationError(code="invalid_padding_policy") + if ( + not isinstance(self.normalization_tolerance, float) + or not math.isfinite(self.normalization_tolerance) + or self.normalization_tolerance < 0.0 + ): + raise ConfigurationError(code="invalid_normalization_tolerance") + + +@dataclass(frozen=True, slots=True) +class CompilerConfig: + pyqpanda_version: str = "0.3.5" + topology: str = "linear" + physical_capacity_multiplier: int = 2 + initial_mapping: str = "identity" + optimization_level: int = 2 + basis_gates: tuple[str, ...] = ("U3", "CNOT") + technical_repetitions: int = 5 + resource_aggregation: str = "median_with_range" + + def __post_init__(self) -> None: + if self.technical_repetitions <= 0: + raise ConfigurationError(code="invalid_technical_repetitions") + if self.physical_capacity_multiplier <= 0: + raise ConfigurationError(code="invalid_physical_capacity") + if not self.basis_gates: + raise ConfigurationError(code="empty_basis_gates") + + +@dataclass(frozen=True, slots=True) +class AnalysisConfig: + basis: str + fidelity_target: float = 0.99 + verification: VerificationLevel = VerificationLevel.STANDARD + methods: tuple[PreparationMethod, ...] = DEFAULT_METHODS + selection_policy: str = SELECTION_POLICY + compiler: CompilerConfig = CompilerConfig() + input_policy: InputPolicy = InputPolicy() + schema_version: str = SCHEMA_VERSION + + def __post_init__(self) -> None: + if self.basis not in {"walsh", "fourier"}: + raise ConfigurationError(code="invalid_basis") + if ( + not isinstance(self.fidelity_target, float) + or not math.isfinite(self.fidelity_target) + or not 0.0 < self.fidelity_target <= 1.0 + ): + raise ConfigurationError(code="invalid_fidelity_target") + if not isinstance(self.verification, VerificationLevel): + raise ConfigurationError(code="invalid_verification") + if not self.methods or not all( + isinstance(method, PreparationMethod) for method in self.methods + ): + raise ConfigurationError(code="invalid_methods") + if self.schema_version != SCHEMA_VERSION: + raise ConfigurationError(code="unsupported_schema_version") + + def canonical_payload(self) -> dict[str, Any]: + """Return the canonical, JSON-ready v1 configuration snapshot.""" + + payload = _canonicalize(self) + if not isinstance(payload, dict): # defensive type narrowing + raise ConfigurationError(code="invalid_analysis_config") + return payload + + +def _canonicalize(value: Any) -> Any: + if isinstance(value, Enum): + return value.value + if is_dataclass(value) and not isinstance(value, type): + return { + field.name: _canonicalize(getattr(value, field.name)) + for field in fields(value) + } + if isinstance(value, Mapping): + if not all(isinstance(key, str) for key in value): + raise ConfigurationError(code="non_string_config_key") + return {key: _canonicalize(item) for key, item in value.items()} + if isinstance(value, (tuple, list)): + return [_canonicalize(item) for item in value] + if isinstance(value, float): + if not math.isfinite(value): + raise ConfigurationError(code="nonfinite_config_float") + return value.hex() + if value is None or isinstance(value, (str, int, bool)): + return value + raise ConfigurationError(code="unsupported_config_value") + + +def canonical_analysis_config_json(config: AnalysisConfig | Mapping[str, Any]) -> str: + """Serialize a scientific configuration under the ADR v1 canonical rules.""" + + canonical = _canonicalize(config) + return json.dumps( + canonical, + ensure_ascii=False, + allow_nan=False, + sort_keys=True, + separators=(",", ":"), + ) + + +def analysis_config_fingerprint( + config: AnalysisConfig | Mapping[str, Any], +) -> str: + """Return the stable ADR v1 analysis-configuration SHA-256.""" + + canonical = canonical_analysis_config_json(config).encode("utf-8") + return hashlib.sha256(_FINGERPRINT_DOMAIN + canonical).hexdigest() + + +__all__ = [ + "SCHEMA_VERSION", + "SELECTION_POLICY", + "PreparationMethod", + "DEFAULT_METHODS", + "EvidenceScopeStatus", + "VerificationLevel", + "SelectionDecision", + "InputPolicy", + "CompilerConfig", + "AnalysisConfig", + "canonical_analysis_config_json", + "analysis_config_fingerprint", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/exceptions.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/exceptions.py new file mode 100644 index 00000000..9dd938d0 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/exceptions.py @@ -0,0 +1,66 @@ +"""Public exception contracts for QSEncode-Insight. + +Phase 1 defines stable names and structured error codes only. Scientific +pipeline code is intentionally absent. +""" + +from __future__ import annotations + + +class QSEncodeInsightError(Exception): + """Base class for structured QSEncode-Insight product errors.""" + + def __init__(self, *, code: str, message: str | None = None) -> None: + if not isinstance(code, str) or not code: + raise ValueError("error code must be a non-empty string") + self.code = code + self.message = message + super().__init__(message if message is not None else code) + + def __str__(self) -> str: + return self.message if self.message is not None else self.code + + +class InputValidationError(QSEncodeInsightError): + """The new facade received an invalid probability input.""" + + +class ConfigurationError(QSEncodeInsightError): + """The new facade received an invalid or unsupported configuration.""" + + +class BaselineConstructionError(QSEncodeInsightError): + """The mandatory dense baseline could not be constructed.""" + + +class ResourceAuditError(QSEncodeInsightError): + """Compiled-resource auditing could not establish a valid result.""" + + +class SerializationError(QSEncodeInsightError): + """A structured result could not be serialized under the v1 schema.""" + + +class ResultBindingError(QSEncodeInsightError): + """A supplied result is not bound to the runtime input/configuration.""" + + +class UncertifiedSelectionError(QSEncodeInsightError): + """Preparation was requested from an uncertified audit selection.""" + + +class InternalInvariantError(QSEncodeInsightError): + """An internal product invariant was violated.""" + + +__all__ = [ + "QSEncodeInsightError", + "InputValidationError", + "ConfigurationError", + "BaselineConstructionError", + "ResourceAuditError", + "SerializationError", + "ResultBindingError", + "UncertifiedSelectionError", + "InternalInvariantError", +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/insight.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/insight.py new file mode 100644 index 00000000..7bc48893 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/insight.py @@ -0,0 +1,385 @@ +"""Public orchestration facade for QSEncode-Insight v1.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import replace +from importlib.metadata import PackageNotFoundError, version +from typing import Any, Literal + +import numpy as np + +from .config import ( + DEFAULT_METHODS, + AnalysisConfig, + CompilerConfig, + EvidenceScopeStatus, + InputPolicy, + PreparationMethod, + VerificationLevel, + analysis_config_fingerprint, +) +from .exceptions import ( + ConfigurationError, + InternalInvariantError, + ResultBindingError, + UncertifiedSelectionError, +) +from .models import ( + CandidateResult, + EvidenceScope, + InsightResult, + PreparationArtifact, + ResourceAudit, + SelectionResult, +) +from ._compiler import ( + FROZEN_COMPILER_PROFILE, + compiler_profile_payload, + compose_end_to_end_program, +) +from ._error_budget import RANKING_POLICY, find_k_star, top_k_coefficients +from ._preparation import adapt_preparation_input, build_preparation +from ._selection import ResourceSelectionRun, run_resource_selection +from ._transforms import analyze_transform +from ._validation import ValidatedInput, canonicalize_probabilities +from ._verification import verify_resource_selection + + +class QSEncodeInsight: + """Analyze and prepare one explicitly selected Walsh or Fourier mode.""" + + def __init__( + self, + *, + basis: Literal["walsh", "fourier"], + fidelity_target: float = 0.99, + verification: Literal["standard", "audit"] = "standard", + methods: Sequence[PreparationMethod] = DEFAULT_METHODS, + compiler: CompilerConfig | None = None, + input_policy: InputPolicy | None = None, + ) -> None: + try: + verification_level = VerificationLevel(verification) + except (TypeError, ValueError) as error: + raise ConfigurationError(code="invalid_verification") from error + + try: + method_tuple = tuple( + method + if isinstance(method, PreparationMethod) + else PreparationMethod(method) + for method in methods + ) + except (TypeError, ValueError) as error: + raise ConfigurationError(code="invalid_methods") from error + + if compiler is not None and not isinstance(compiler, CompilerConfig): + raise ConfigurationError(code="invalid_compiler_config") + if input_policy is not None and not isinstance(input_policy, InputPolicy): + raise ConfigurationError(code="invalid_input_policy") + + self._config = AnalysisConfig( + basis=basis, + fidelity_target=fidelity_target, + verification=verification_level, + methods=method_tuple, + compiler=compiler if compiler is not None else CompilerConfig(), + input_policy=input_policy if input_policy is not None else InputPolicy(), + ) + self._analysis_config_fingerprint = analysis_config_fingerprint(self._config) + + @property + def basis(self) -> str: + return self._config.basis + + @property + def fidelity_target(self) -> float: + return self._config.fidelity_target + + @property + def verification(self) -> VerificationLevel: + return self._config.verification + + @property + def methods(self) -> tuple[PreparationMethod, ...]: + return self._config.methods + + @property + def compiler(self) -> CompilerConfig: + return self._config.compiler + + @property + def input_policy(self) -> InputPolicy: + return self._config.input_policy + + @property + def analysis_config_fingerprint(self) -> str: + return self._analysis_config_fingerprint + + def analyze(self, probabilities: Any) -> InsightResult: + validated = canonicalize_probabilities( + probabilities, policy=self.input_policy + ) + amplitudes = np.sqrt(validated.probabilities) + coefficients, transform = analyze_transform( + amplitudes, + basis=self.basis, + oracle_check=self.basis == "walsh" and validated.probabilities.size <= 64, + ) + error_budget = find_k_star(coefficients, self.fidelity_target) + resource_run = run_resource_selection( + coefficients, + basis=self.basis, + error_budget=error_budget, + profile=self.compiler, + methods=self.methods, + ) + verification = verify_resource_selection( + coefficients, + basis=self.basis, + run=resource_run, + level=self.verification, + ) + evidence = self._evidence_scope( + padded_length=validated.summary.padded_length, + transform_implementation=transform.implementation, + ranking_policy=error_budget.ranking_policy, + ) + candidates = tuple( + _public_candidate(candidate) for candidate in resource_run.grid.candidates + ) + capabilities = _unique_capabilities(candidates) + return InsightResult( + input=validated.summary, + analysis_config=self._config.canonical_payload(), + analysis_config_fingerprint=self.analysis_config_fingerprint, + transform=transform, + error_budget=error_budget, + capabilities=capabilities, + candidates=candidates, + selection=_public_selection(resource_run.selection), + semantic_verification=verification, + attribution=resource_run.attribution, + evidence_scope=evidence, + ) + + def prepare( + self, + probabilities: Any, + *, + result: InsightResult | None = None, + fallback_to_baseline: bool = False, + ) -> PreparationArtifact: + if result is None: + return self.prepare( + probabilities, + result=self.analyze(probabilities), + fallback_to_baseline=fallback_to_baseline, + ) + + validated = canonicalize_probabilities( + probabilities, policy=self.input_policy + ) + self._validate_result_binding(validated, result) + if result.selection is None or result.semantic_verification is None: + raise ResultBindingError(code="configuration_mismatch") + + audit_invalid = ( + self.verification is VerificationLevel.AUDIT + and not result.semantic_verification.recommendation_valid + ) + if audit_invalid and not fallback_to_baseline: + raise UncertifiedSelectionError(code="audit_recommendation_invalid") + + coefficients, _ = analyze_transform( + np.sqrt(validated.probabilities), basis=self.basis, oracle_check=False + ) + fallback = bool(audit_invalid and fallback_to_baseline) + if fallback or result.selection.decision.value == "do_not_compress": + candidate_id = "dense_full__amplitude_encode" + method = PreparationMethod.AMPLITUDE_ENCODE + selected_coefficients = coefficients + artifact_k = None + audit = result.selection.baseline_resource + else: + candidate_id = result.selection.selected_candidate_id + if candidate_id is None or result.selection.method is None or result.selection.k is None: + raise InternalInvariantError(code="selected_artifact_metadata_missing") + method = result.selection.method + artifact_k = result.selection.k + selected_coefficients = top_k_coefficients( + coefficients, artifact_k, normalize=True + ) + selected = next( + (item for item in result.candidates if item.candidate_id == candidate_id), + None, + ) + if selected is None: + raise InternalInvariantError(code="selected_artifact_candidate_missing") + audit = selected.resource_audit + + prepared = adapt_preparation_input(selected_coefficients) + build = build_preparation(method, prepared) + end_to_end = compose_end_to_end_program( + build, basis=self.basis, profile=self.compiler + ) + provenance: dict[str, Any] = { + "analysis_config_fingerprint": result.analysis_config_fingerprint, + "effective_probability_sha256": result.input.effective_probability_sha256, + "artifact_kind": ( + "fallback_from_uncertified_selection" + if fallback + else "dense_full_baseline" + if result.selection.decision.value == "do_not_compress" + else "selected_compressed_candidate" + ), + "selection_reason_code": result.selection.reason_code, + } + if fallback: + provenance.update( + { + "original_selected_candidate_id": result.selection.selected_candidate_id, + "audit_status": result.semantic_verification.status, + "fallback_explicitly_requested": True, + } + ) + compiler_metadata = { + "profile": compiler_profile_payload(self.compiler), + "profile_fingerprint": ( + audit.compiler_profile_fingerprint if audit is not None else None + ), + } + verification_status = ( + "fallback_from_uncertified_selection" + if fallback + else "audit_certified_5_of_5" + if self.verification is VerificationLevel.AUDIT + else "standard_validated" + ) + return PreparationArtifact( + program=end_to_end.program, + output_qubits=end_to_end.output_qubits, + ancillas=end_to_end.ancillas, + selected_candidate_id=candidate_id, + decision=result.selection.decision, + decision_reason=result.selection.reason_code, + basis=self.basis, + k=artifact_k, + compiler_metadata=compiler_metadata, + verification_status=verification_status, + evidence_scope=result.evidence_scope, + provenance=provenance, + ) + + def _validate_result_binding( + self, validated: ValidatedInput, result: InsightResult + ) -> None: + if not isinstance(result, InsightResult): + raise ResultBindingError(code="configuration_mismatch") + if result.schema_version != InsightResult.SCHEMA_VERSION: + raise ResultBindingError(code="configuration_mismatch") + if ( + validated.summary.effective_probability_sha256 + != result.input.effective_probability_sha256 + ): + raise ResultBindingError(code="input_mismatch") + if self.analysis_config_fingerprint != result.analysis_config_fingerprint: + raise ResultBindingError(code="configuration_mismatch") + + def _evidence_scope( + self, + *, + padded_length: int, + transform_implementation: str, + ranking_policy: str, + ) -> EvidenceScope: + reasons: list[str] = [] + if self.fidelity_target != 0.99: + reasons.append("fidelity_target_not_0.99") + if self.methods != DEFAULT_METHODS: + reasons.append("methods_or_order_not_frozen_default") + if self._config.selection_policy != "frozen_lexicographic_v1": + reasons.append("selection_policy_not_frozen_default") + if self.compiler != FROZEN_COMPILER_PROFILE: + reasons.append("compiler_profile_not_frozen_default") + if padded_length not in {8, 16, 32, 64}: + reasons.append("dimension_outside_validated_scope") + expected_transform = ( + "iterative_fwht_v1" if self.basis == "walsh" else "scipy_fft_ortho_v1" + ) + if transform_implementation != expected_transform: + reasons.append("transform_convention_mismatch") + if ranking_policy != RANKING_POLICY: + reasons.append("ranking_policy_mismatch") + try: + runtime_version = version("pyqpanda3") + except PackageNotFoundError: + runtime_version = "unavailable" + if runtime_version != "0.3.5": + reasons.append("pyqpanda_runtime_not_0.3.5") + status = ( + EvidenceScopeStatus.VALIDATED_DEFAULT + if not reasons + else EvidenceScopeStatus.OUTSIDE_VALIDATED_SCOPE + ) + return EvidenceScope( + status=status, + reasons=tuple(reasons), + reference="Generalization Benchmark Protocol v1.1 / Locked Test PASS", + ) + + +def _public_attempt(attempt: Any) -> dict[str, Any]: + return { + "attempt_index": attempt.attempt_index, + "success": attempt.success, + "status": attempt.status, + "originir_sha256": attempt.originir_sha256, + "compiled_depth": attempt.compiled_depth, + "compiled_total_gates": attempt.compiled_total_gates, + "compiled_one_qubit_gates": attempt.compiled_one_qubit_gates, + "compiled_two_qubit_gates": attempt.compiled_two_qubit_gates, + "compiled_cnot_gates": attempt.compiled_cnot_gates, + "exception_type": attempt.exception_type, + "exception_message": attempt.exception_message, + "diagnostics": attempt.diagnostics, + } + + +def _public_resource(audit: ResourceAudit | None) -> ResourceAudit | None: + if audit is None: + return None + return replace( + audit, + compilation_attempts=tuple( + _public_attempt(attempt) for attempt in audit.compilation_attempts + ), + ) + + +def _public_candidate(candidate: CandidateResult) -> CandidateResult: + return replace(candidate, resource_audit=_public_resource(candidate.resource_audit)) + + +def _public_selection(selection: SelectionResult) -> SelectionResult: + return replace( + selection, + baseline_resource=_public_resource(selection.baseline_resource), + best_compressed_resource=_public_resource(selection.best_compressed_resource), + ) + + +def _unique_capabilities( + candidates: tuple[CandidateResult, ...], +) -> tuple[Any, ...]: + seen: set[PreparationMethod] = set() + reports = [] + for candidate in candidates: + if candidate.capability is not None and candidate.method not in seen: + reports.append(candidate.capability) + seen.add(candidate.method) + return tuple(reports) + + +__all__ = ["QSEncodeInsight"] diff --git a/pyqpanda-algorithm/pyqpanda_alg/QSEncode/models.py b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/models.py new file mode 100644 index 00000000..903c3a97 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QSEncode/models.py @@ -0,0 +1,337 @@ +"""Immutable schema skeletons for QSEncode-Insight v1. + +The types reserve the public result contract without fabricating Phase 2+ +scientific values. Nested records use tuples for collection fields; the +top-level analysis-config mapping is a serialized provenance snapshot and must +be treated as read-only by producers and consumers. +""" + +from __future__ import annotations + +from dataclasses import dataclass, fields, is_dataclass +from enum import Enum +import json +import math +from types import MappingProxyType +from typing import Any, ClassVar, Mapping + +from .config import ( + SCHEMA_VERSION, + EvidenceScopeStatus, + PreparationMethod, + SelectionDecision, + VerificationLevel, +) +from .exceptions import SerializationError + + +def _deep_freeze(value: Any) -> Any: + if isinstance(value, Mapping): + return MappingProxyType( + {str(key): _deep_freeze(item) for key, item in value.items()} + ) + if isinstance(value, (list, tuple)): + return tuple(_deep_freeze(item) for item in value) + if isinstance(value, set): + return tuple(sorted((_deep_freeze(item) for item in value), key=repr)) + return value + + +def _freeze_fields(instance: Any, *names: str) -> None: + for name in names: + value = getattr(instance, name) + if value is not None: + object.__setattr__(instance, name, _deep_freeze(value)) + + +def _serialize(value: Any) -> Any: + if isinstance(value, Enum): + return value.value + if is_dataclass(value) and not isinstance(value, type): + return {field.name: _serialize(getattr(value, field.name)) for field in fields(value)} + if isinstance(value, Mapping): + return {str(key): _serialize(item) for key, item in value.items()} + if isinstance(value, (tuple, list)): + return [_serialize(item) for item in value] + if value is None or isinstance(value, (str, int, bool)): + return value + if isinstance(value, float): + if not math.isfinite(value): + raise SerializationError(code="nonfinite_result_float") + return value + raise SerializationError( + code="unsupported_result_value", + message=f"Unsupported structured-result value: {type(value).__name__}", + ) + + +@dataclass(frozen=True, slots=True) +class InputSummary: + original_length: int + padded_length: int + original_sum: float + normalized: bool + padding_count: int + original_input_sha256: str + effective_probability_sha256: str + + +@dataclass(frozen=True, slots=True) +class TransformDiagnostics: + basis: str + coefficient_count: int + parseval_error: float + implementation: str + normalization: str + oracle_checked: bool + oracle_max_abs_error: float | None = None + + +@dataclass(frozen=True, slots=True) +class ErrorBudgetResult: + fidelity_target: float + k_star: int + retained_energy: float + previous_retained_energy: float + candidate_k: tuple[int, ...] = () + minimality_pass: bool = False + ranking_policy: str = "frozen_stable_v1" + + def __post_init__(self) -> None: + _freeze_fields(self, "candidate_k") + + +@dataclass(frozen=True, slots=True) +class CapabilityReport: + method: PreparationMethod + compatible: bool + reason_code: str | None = None + reason: str | None = None + required_qubits: int | None = None + ancillas: tuple[int, ...] = () + input_constraints: tuple[str, ...] = () + observed_output_qubits: tuple[int, ...] = () + exception_type: str | None = None + exception_message: str | None = None + diagnostics: Mapping[str, Any] | None = None + failure_stage: str | None = None + logical_fidelity: float | None = None + + def __post_init__(self) -> None: + _freeze_fields( + self, + "ancillas", + "input_constraints", + "observed_output_qubits", + "diagnostics", + ) + + +@dataclass(frozen=True, slots=True) +class ResourceAudit: + compiled_two_qubit_gates: float | None + compiled_depth: float | None + compiled_total_gates: float | None + required_qubits: int + allocated_qubits: int + repetitions: int + compiled_one_qubit_gates: float | None = None + compiled_cnot_gates: float | None = None + ancillas: tuple[int, ...] = () + two_qubit_range: tuple[float, float] | None = None + depth_range: tuple[float, float] | None = None + total_gate_range: tuple[float, float] | None = None + one_qubit_range: tuple[float, float] | None = None + q_required_times_depth: float | None = None + q_allocated_times_depth: float | None = None + successful_attempts: int = 0 + failed_attempts: int = 0 + compiler_profile: Mapping[str, Any] | None = None + compiler_profile_fingerprint: str | None = None + valid: bool = False + status: str = "not_run" + failure_reason: str | None = None + compilation_attempts: tuple[Any, ...] = () + + def __post_init__(self) -> None: + _freeze_fields(self, "compiler_profile") + _freeze_fields( + self, + "ancillas", + "two_qubit_range", + "depth_range", + "total_gate_range", + "one_qubit_range", + "compilation_attempts", + ) + + +@dataclass(frozen=True, slots=True) +class CandidateResult: + candidate_id: str + method: PreparationMethod + k: int + status: str + verified_fidelity: float | None = None + resource_audit: ResourceAudit | None = None + failure_reason: str | None = None + capability: CapabilityReport | None = None + retained_fidelity: float | None = None + eligible: bool = False + eligibility_reason: str | None = None + role: str = "compressed" + + +@dataclass(frozen=True, slots=True) +class SelectionResult: + decision: SelectionDecision + reason_code: str + selected_candidate_id: str | None = None + method: PreparationMethod | None = None + k: int | None = None + reason: str | None = None + baseline_resource: ResourceAudit | None = None + best_compressed_candidate_id: str | None = None + best_compressed_resource: ResourceAudit | None = None + comparison_metrics: Mapping[str, Any] | None = None + + def __post_init__(self) -> None: + _freeze_fields(self, "comparison_metrics") + + +@dataclass(frozen=True, slots=True) +class SemanticVerificationAttempt: + attempt_index: int + status: str + fidelity: float | None + mapping_method: str | None + output_qubits: tuple[int, ...] + mapping: tuple[int, ...] = () + ancilla_treatment: str = "partial_trace" + exception_type: str | None = None + exception_message: str | None = None + diagnostics: Mapping[str, Any] | None = None + + def __post_init__(self) -> None: + _freeze_fields(self, "output_qubits", "mapping", "diagnostics") + + +@dataclass(frozen=True, slots=True) +class SemanticVerification: + level: VerificationLevel + status: str + recommendation_valid: bool + minimum_fidelity: float | None = None + technical_repetitions: int = 0 + selected_candidate_id: str | None = None + attempts: tuple[SemanticVerificationAttempt, ...] = () + + def __post_init__(self) -> None: + _freeze_fields(self, "attempts") + + +@dataclass(frozen=True, slots=True) +class AttributionReport: + truncation_two_qubit_gain: float | None = None + preparation_two_qubit_gain: float | None = None + truncation_depth_gain: float | None = None + preparation_depth_gain: float | None = None + total_two_qubit_difference: float | None = None + truncation_two_qubit_difference: float | None = None + preparation_two_qubit_difference: float | None = None + total_depth_difference: float | None = None + truncation_depth_difference: float | None = None + preparation_depth_difference: float | None = None + two_qubit_identity_error: float | None = None + depth_identity_error: float | None = None + + +@dataclass(frozen=True, slots=True) +class EvidenceScope: + status: EvidenceScopeStatus + reasons: tuple[str, ...] + reference: str | None + + def __post_init__(self) -> None: + _freeze_fields(self, "reasons") + + +@dataclass(frozen=True, slots=True) +class InsightResult: + SCHEMA_VERSION: ClassVar[str] = SCHEMA_VERSION + + input: InputSummary + analysis_config: Mapping[str, Any] + analysis_config_fingerprint: str + transform: TransformDiagnostics | None = None + error_budget: ErrorBudgetResult | None = None + capabilities: tuple[CapabilityReport, ...] = () + candidates: tuple[CandidateResult, ...] = () + selection: SelectionResult | None = None + semantic_verification: SemanticVerification | None = None + attribution: AttributionReport | None = None + evidence_scope: EvidenceScope | None = None + schema_version: str = SCHEMA_VERSION + + def __post_init__(self) -> None: + if self.schema_version != self.SCHEMA_VERSION: + raise ValueError("unsupported InsightResult schema_version") + _freeze_fields(self, "analysis_config", "capabilities", "candidates") + + def to_dict(self) -> dict[str, Any]: + serialized = _serialize(self) + if not isinstance(serialized, dict): + raise SerializationError(code="invalid_result_root") + return serialized + + def to_json(self, *, indent: int | None = None) -> str: + return json.dumps( + self.to_dict(), + ensure_ascii=False, + allow_nan=False, + sort_keys=True, + separators=(",", ":") if indent is None else None, + indent=indent, + ) + + +@dataclass(frozen=True, slots=True) +class PreparationArtifact: + program: Any + output_qubits: tuple[int, ...] + ancillas: tuple[int, ...] + selected_candidate_id: str + decision: SelectionDecision + decision_reason: str + basis: str + k: int | None + compiler_metadata: Mapping[str, Any] + verification_status: str + evidence_scope: EvidenceScope + provenance: Mapping[str, Any] + + def __post_init__(self) -> None: + _freeze_fields( + self, + "output_qubits", + "ancillas", + "compiler_metadata", + "provenance", + ) + + +__all__ = [ + "InputSummary", + "TransformDiagnostics", + "ErrorBudgetResult", + "CapabilityReport", + "CandidateResult", + "SelectionResult", + "ResourceAudit", + "SemanticVerificationAttempt", + "SemanticVerification", + "AttributionReport", + "EvidenceScope", + "InsightResult", + "PreparationArtifact", +] diff --git a/pyqpanda-algorithm/pytest-qseencode.ini b/pyqpanda-algorithm/pytest-qseencode.ini new file mode 100644 index 00000000..8a3edd90 --- /dev/null +++ b/pyqpanda-algorithm/pytest-qseencode.ini @@ -0,0 +1,8 @@ +[pytest] +testpaths = + ../test/QAlgBase +python_files = + test_qseencode_*.py + Test_class_basic_sparecode_QSpare_Code.py +addopts = -q -p no:cacheprovider +log_level = INFO diff --git a/pyqpanda-algorithm/requirements-qseencode-insight-test.txt b/pyqpanda-algorithm/requirements-qseencode-insight-test.txt new file mode 100644 index 00000000..e53d1060 --- /dev/null +++ b/pyqpanda-algorithm/requirements-qseencode-insight-test.txt @@ -0,0 +1,2 @@ +-r requirements-qseencode-insight.txt +pytest==9.0.3 diff --git a/pyqpanda-algorithm/requirements-qseencode-insight.txt b/pyqpanda-algorithm/requirements-qseencode-insight.txt new file mode 100644 index 00000000..fcf972ac --- /dev/null +++ b/pyqpanda-algorithm/requirements-qseencode-insight.txt @@ -0,0 +1,11 @@ +# Reproduces the validated QSEncode-Insight contest environment. +# Use a dedicated virtual environment; the repository-wide requirements remain +# intentionally broader for upstream compatibility. +numpy==2.4.6 +scipy==1.17.1 +sympy==1.14.0 +matplotlib==3.10.9 +# The repository package initializer eagerly imports QSVD and QSVR. +pandas==3.0.3 +scikit-learn==1.8.0 +pyqpanda3==0.3.5 diff --git a/test/QAlgBase/test_qseencode_attribution.py b/test/QAlgBase/test_qseencode_attribution.py new file mode 100644 index 00000000..f9b3b555 --- /dev/null +++ b/test/QAlgBase/test_qseencode_attribution.py @@ -0,0 +1,37 @@ +from pyqpanda_alg.QSEncode import CandidateResult, PreparationMethod, ResourceAudit +from pyqpanda_alg.QSEncode._selection import compute_attribution + + +def _candidate(name, method, k, twoq, depth): + audit = ResourceAudit( + compiled_two_qubit_gates=float(twoq), compiled_depth=float(depth), + compiled_total_gates=float(twoq + depth), required_qubits=2, + allocated_qubits=4, repetitions=5, valid=True, status="valid", + ) + return CandidateResult(name, method, k, "eligible", resource_audit=audit, eligible=True) + + +def test_attribution_absolute_identity_is_exact_for_both_resources(): + baseline = _candidate("full", PreparationMethod.AMPLITUDE_ENCODE, 8, 100, 200) + dense = _candidate("dense-k", PreparationMethod.AMPLITUDE_ENCODE, 4, 70, 140) + selected = _candidate("sparse-k", PreparationMethod.SPARSE_ISOMETRY, 4, 40, 90) + result = compute_attribution(baseline, dense, selected) + + assert result.total_two_qubit_difference == 60 + assert result.truncation_two_qubit_difference == 30 + assert result.preparation_two_qubit_difference == 30 + assert result.total_depth_difference == 110 + assert result.truncation_depth_difference == 60 + assert result.preparation_depth_difference == 50 + assert result.two_qubit_identity_error == 0 + assert result.depth_identity_error == 0 + + +def test_amplitude_winner_has_exact_zero_preparation_contribution(): + baseline = _candidate("full", PreparationMethod.AMPLITUDE_ENCODE, 8, 100, 200) + selected = _candidate("dense-k", PreparationMethod.AMPLITUDE_ENCODE, 4, 70, 140) + result = compute_attribution(baseline, selected, selected) + assert result.preparation_two_qubit_difference == 0 + assert result.preparation_depth_difference == 0 + assert result.two_qubit_identity_error == 0 + assert result.depth_identity_error == 0 diff --git a/test/QAlgBase/test_qseencode_candidate_pipeline.py b/test/QAlgBase/test_qseencode_candidate_pipeline.py new file mode 100644 index 00000000..a3f6bd9a --- /dev/null +++ b/test/QAlgBase/test_qseencode_candidate_pipeline.py @@ -0,0 +1,112 @@ +import numpy as np + +from pyqpanda_alg.QSEncode import ( + DEFAULT_METHODS, + PreparationMethod, + ResourceAudit, + SelectionDecision, +) +from pyqpanda_alg.QSEncode._error_budget import find_k_star +from pyqpanda_alg.QSEncode._selection import generate_candidate_grid, run_resource_selection +from pyqpanda_alg.QSEncode._transforms import normalized_fourier, normalized_fwht + + +PROBABILITIES = np.array( + [0.0006917643261373052, 0.015724004731018214, 0.1261730210273901, + 0.3574112099154543, 0.3574112099154544, 0.1261730210273902, + 0.01572400473101823, 0.0006917643261373052] +) + + +def test_candidate_grid_preserves_every_k_method_and_same_compiler_profile(): + coefficients = normalized_fwht(np.sqrt(PROBABILITIES)) + budget = find_k_star(coefficients, 0.99) + grid = generate_candidate_grid(coefficients, basis="walsh", error_budget=budget) + + assert grid.dense_full.role == "dense_full" + assert grid.dense_full.resource_audit.valid + assert len(grid.candidates) == len(budget.candidate_k) * len(DEFAULT_METHODS) + assert {(candidate.k, candidate.method) for candidate in grid.candidates} == { + (k, method) for k in budget.candidate_k for method in DEFAULT_METHODS + } + fingerprints = { + candidate.resource_audit.compiler_profile_fingerprint + for candidate in (grid.dense_full, *grid.candidates) + if candidate.resource_audit is not None + } + assert len(fingerprints) == 1 + + +def test_end_to_end_resource_selection_returns_explainable_result_and_attribution(): + coefficients = normalized_fwht(np.sqrt(PROBABILITIES)) + budget = find_k_star(coefficients, 0.99) + result = run_resource_selection(coefficients, basis="walsh", error_budget=budget) + + assert result.selection.decision in {SelectionDecision.COMPRESS, SelectionDecision.DO_NOT_COMPRESS} + assert result.selection.baseline_resource.valid + assert len(result.grid.candidates) == len(budget.candidate_k) * len(DEFAULT_METHODS) + if result.selection.decision is SelectionDecision.COMPRESS: + assert result.attribution is not None + assert result.attribution.two_qubit_identity_error == 0 + assert result.attribution.depth_identity_error == 0 + + +def test_resource_invalid_method_candidate_is_retained_in_grid(monkeypatch): + coefficients = normalized_fwht(np.sqrt(PROBABILITIES)) + budget = find_k_star(coefficients, 0.99) + + def synthetic_audit(build, **kwargs): + failed = build.method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION + return ResourceAudit( + compiled_two_qubit_gates=None if failed else 10.0, + compiled_depth=None if failed else 20.0, + compiled_total_gates=None if failed else 30.0, + required_qubits=build.required_qubits, + allocated_qubits=6, + repetitions=5, + ancillas=build.ancillas, + successful_attempts=0 if failed else 5, + failed_attempts=5 if failed else 0, + valid=not failed, + status="compile_failure" if failed else "valid", + ) + + monkeypatch.setattr( + "pyqpanda_alg.QSEncode._selection.audit_build_resources", synthetic_audit + ) + grid = generate_candidate_grid(coefficients, basis="walsh", error_budget=budget) + + ds_candidates = [ + candidate for candidate in grid.candidates + if candidate.method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION + ] + assert len(ds_candidates) == len(budget.candidate_k) + fidelity_eligible_ds = [ + candidate for candidate in ds_candidates + if candidate.retained_fidelity >= 0.99 - 1e-12 + ] + assert fidelity_eligible_ds + assert all(candidate.status == "resource_invalid" for candidate in fidelity_eligible_ds) + assert all(candidate.eligible is False for candidate in ds_candidates) + assert all(candidate.resource_audit is not None for candidate in ds_candidates) + + +def test_frozen_fourier_case_compresses_with_exact_attribution_identity(): + coefficients = normalized_fourier(np.sqrt(PROBABILITIES)) + budget = find_k_star(coefficients, 0.99) + result = run_resource_selection( + coefficients, basis="fourier", error_budget=budget + ) + + assert result.selection.decision is SelectionDecision.COMPRESS + assert result.selection.selected_candidate_id == "compressed__k4__sparse_isometry" + assert result.attribution is not None + assert result.attribution.two_qubit_identity_error == 0 + assert result.attribution.depth_identity_error == 0 + ds_k4 = next( + candidate for candidate in result.grid.candidates + if candidate.k == 4 + and candidate.method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION + ) + assert ds_k4.status == "capability_incompatible" + assert ds_k4 in result.grid.candidates diff --git a/test/QAlgBase/test_qseencode_capability.py b/test/QAlgBase/test_qseencode_capability.py new file mode 100644 index 00000000..2c37a18e --- /dev/null +++ b/test/QAlgBase/test_qseencode_capability.py @@ -0,0 +1,157 @@ +from dataclasses import replace + +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode import DEFAULT_METHODS, PreparationMethod +from pyqpanda_alg.QSEncode.exceptions import InternalInvariantError +from pyqpanda_alg.QSEncode._capability import ( + CAPABILITY_REASON_CODES, + assess_all_capabilities, + assess_capability, +) +from pyqpanda_alg.QSEncode._preparation import ( + adapt_preparation_input, + build_preparation, +) + + +def test_capability_reports_are_retained_in_default_method_order(): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + assessments = assess_all_capabilities(prepared_input) + + assert tuple(item.report.method for item in assessments) == DEFAULT_METHODS + assert len(assessments) == 3 + assert all(item.report.reason_code in CAPABILITY_REASON_CODES for item in assessments) + + +def test_mixed_compatible_and_incompatible_candidates_are_not_dropped(): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + + assessments = assess_all_capabilities(prepared_input, available_qubits=3) + + assert len(assessments) == 3 + assert [item.report.compatible for item in assessments] == [True, True, False] + assert assessments[-1].report.method is PreparationMethod.DS_QUANTUM_STATE_PREPARATION + assert assessments[-1].report.reason_code == "insufficient_qubits" + + +def test_insufficient_qubits_is_static_structured_incompatibility(): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + assessment = assess_capability( + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + prepared_input, + available_qubits=3, + ) + + assert assessment.build is None + assert assessment.report.compatible is False + assert assessment.report.reason_code == "insufficient_qubits" + assert assessment.report.failure_stage == "static" + assert assessment.report.required_qubits == 4 + + +def test_invalid_sparse_keys_are_reported_without_top_level_exception(): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + malformed = replace(prepared_input, sparse_items=(("2x", 1.0),)) + + assessment = assess_capability(PreparationMethod.SPARSE_ISOMETRY, malformed) + + assert assessment.report.compatible is False + assert assessment.report.reason_code == "invalid_sparse_keys" + assert assessment.report.failure_stage == "static" + assert assessment.build is None + + +def test_backend_constructor_failure_is_structured(monkeypatch): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + + def reject(*args, **kwargs): + raise RuntimeError("synthetic constructor rejection") + + monkeypatch.setattr("pyqpanda_alg.QSEncode._capability.build_preparation", reject) + assessment = assess_capability(PreparationMethod.SPARSE_ISOMETRY, prepared_input) + + assert assessment.report.compatible is False + assert assessment.report.reason_code == "constructor_rejected_input" + assert assessment.report.failure_stage == "construction" + assert assessment.report.exception_type == "RuntimeError" + assert "synthetic" in assessment.report.exception_message + + +def test_mandatory_amplitude_baseline_failure_is_preserved_for_future_escalation(monkeypatch): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + + def reject(*args, **kwargs): + raise ValueError("synthetic baseline rejection") + + monkeypatch.setattr("pyqpanda_alg.QSEncode._capability.build_preparation", reject) + assessment = assess_capability(PreparationMethod.AMPLITUDE_ENCODE, prepared_input) + + assert assessment.report.method is PreparationMethod.AMPLITUDE_ENCODE + assert assessment.report.compatible is False + assert assessment.report.reason_code == "constructor_rejected_input" + assert assessment.report.exception_type == "ValueError" + assert assessment.build is None + + +def test_logical_fidelity_mismatch_is_a_structured_candidate_result(monkeypatch): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + monkeypatch.setattr( + "pyqpanda_alg.QSEncode._capability._logical_state_fidelity", + lambda *args, **kwargs: 0.5, + ) + + assessment = assess_capability(PreparationMethod.SPARSE_ISOMETRY, prepared_input) + + assert assessment.report.compatible is False + assert assessment.report.reason_code == "logical_fidelity_mismatch" + assert assessment.report.failure_stage == "correctness" + assert assessment.report.logical_fidelity == 0.5 + assert assessment.build is not None + + +def test_internal_invariant_error_from_build_propagates_unchanged(monkeypatch): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + internal_failure = InternalInvariantError(code="synthetic_internal_failure") + + def fail_with_internal_invariant(*args, **kwargs): + raise internal_failure + + monkeypatch.setattr( + "pyqpanda_alg.QSEncode._capability.build_preparation", + fail_with_internal_invariant, + ) + + with pytest.raises(InternalInvariantError) as exc_info: + assess_capability(PreparationMethod.AMPLITUDE_ENCODE, prepared_input) + + assert exc_info.value is internal_failure + assert exc_info.value.code == "synthetic_internal_failure" + + +def test_real_duplicate_output_metadata_is_rejected_by_capability_gate(monkeypatch): + prepared_input = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + valid_build = build_preparation( + PreparationMethod.AMPLITUDE_ENCODE, prepared_input + ) + malformed_build = replace( + valid_build, + output_qubits=(0, 1, 0, 1), + diagnostics={ + **valid_build.diagnostics, + "backend_reported_output_qubits": (0, 1, 0, 1), + }, + ) + monkeypatch.setattr( + "pyqpanda_alg.QSEncode._capability.build_preparation", + lambda *args, **kwargs: malformed_build, + ) + + assessment = assess_capability( + PreparationMethod.AMPLITUDE_ENCODE, prepared_input + ) + + assert assessment.report.compatible is False + assert assessment.report.reason_code == "unexpected_output_register" + assert assessment.report.failure_stage == "construction" diff --git a/test/QAlgBase/test_qseencode_cli.py b/test/QAlgBase/test_qseencode_cli.py new file mode 100644 index 00000000..45dc7a5e --- /dev/null +++ b/test/QAlgBase/test_qseencode_cli.py @@ -0,0 +1,74 @@ +import json + +import pytest + +from pyqpanda_alg.QSEncode.cli import main + + +PROBABILITIES = [ + 0.0006917643261373052, + 0.015724004731018214, + 0.1261730210273901, + 0.3574112099154543, + 0.3574112099154544, + 0.1261730210273902, + 0.01572400473101823, + 0.0006917643261373052, +] + + +def test_cli_help(capsys): + with pytest.raises(SystemExit) as error: + main(["--help"]) + assert error.value.code == 0 + assert "QSEncode-Insight" in capsys.readouterr().out + + +def test_cli_valid_standard_analysis_outputs_json(capsys): + exit_code = main([ + "analyze", + "--basis", "fourier", + "--input-json", json.dumps(PROBABILITIES), + ]) + + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["selection"]["decision"] == "compress" + assert payload["selection"]["selected_candidate_id"] == "compressed__k4__sparse_isometry" + assert payload["semantic_verification"]["status"] == "not_run_by_standard" + + +def test_cli_invalid_basis_is_rejected(capsys): + with pytest.raises(SystemExit) as error: + main([ + "analyze", + "--basis", "auto", + "--input-json", "[0.5, 0.5]", + ]) + assert error.value.code == 2 + assert "invalid choice" in capsys.readouterr().err + + +@pytest.mark.parametrize("value", ["not-json", '{"not": "a list"}']) +def test_cli_json_input_failure_is_structured(value, capsys): + with pytest.raises(SystemExit) as error: + main([ + "analyze", + "--basis", "walsh", + "--input-json", value, + ]) + assert error.value.code == 2 + assert "probability input" in capsys.readouterr().err + + +def test_cli_reads_json_file(tmp_path, capsys): + path = tmp_path / "probabilities.json" + path.write_text(json.dumps(PROBABILITIES), encoding="utf-8") + + assert main([ + "analyze", + "--basis", "walsh", + "--input-file", str(path), + ]) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["selection"]["decision"] == "do_not_compress" diff --git a/test/QAlgBase/test_qseencode_compiler_resources.py b/test/QAlgBase/test_qseencode_compiler_resources.py new file mode 100644 index 00000000..0da80f65 --- /dev/null +++ b/test/QAlgBase/test_qseencode_compiler_resources.py @@ -0,0 +1,109 @@ +import pytest + +from pyqpanda3.transpilation import Transpiler + +from pyqpanda_alg.QSEncode import CompilerConfig, PreparationMethod +from pyqpanda_alg.QSEncode._capability import assess_capability +from pyqpanda_alg.QSEncode._compiler import ( + FROZEN_COMPILER_PROFILE, + TECHNICAL_REPETITIONS, + compile_five_repetitions, + compose_end_to_end_program, +) +from pyqpanda_alg.QSEncode._preparation import adapt_preparation_input +from pyqpanda_alg.QSEncode._resources import audit_build_resources +from pyqpanda_alg.QSEncode.exceptions import InternalInvariantError + + +def _compatible_build(method=PreparationMethod.AMPLITUDE_ENCODE): + prepared = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + assessment = assess_capability(method, prepared) + assert assessment.report.compatible and assessment.build is not None + return assessment.build + + +def test_frozen_compiler_profile_is_exact(): + assert FROZEN_COMPILER_PROFILE == CompilerConfig() + assert FROZEN_COMPILER_PROFILE.pyqpanda_version == "0.3.5" + assert FROZEN_COMPILER_PROFILE.topology == "linear" + assert FROZEN_COMPILER_PROFILE.physical_capacity_multiplier == 2 + assert FROZEN_COMPILER_PROFILE.initial_mapping == "identity" + assert FROZEN_COMPILER_PROFILE.optimization_level == 2 + assert FROZEN_COMPILER_PROFILE.basis_gates == ("U3", "CNOT") + assert FROZEN_COMPILER_PROFILE.technical_repetitions == 5 + assert FROZEN_COMPILER_PROFILE.resource_aggregation == "median_with_range" + + +@pytest.mark.parametrize("method", list(PreparationMethod)) +def test_end_to_end_program_separates_required_and_allocated_width(method): + compilation = compose_end_to_end_program(_compatible_build(method), basis="walsh") + + assert compilation.allocated_qubits == 4 + assert compilation.output_qubits == (2, 3) + assert compilation.required_qubits == (4 if method.value.startswith("ds_") else 2) + assert len(compilation.ancillas) == (2 if method.value.startswith("ds_") else 0) + assert compilation.program is not None + + +def test_five_real_compilations_record_complete_u3_cnot_resources(): + compilation = compose_end_to_end_program(_compatible_build(), basis="walsh") + attempts = compile_five_repetitions(compilation) + + assert TECHNICAL_REPETITIONS == 5 + assert len(attempts) == 5 + assert tuple(attempt.attempt_index for attempt in attempts) == (0, 1, 2, 3, 4) + assert all(attempt.success for attempt in attempts) + assert all(attempt.compiled_program is not None for attempt in attempts) + assert all(attempt.compiled_originir for attempt in attempts) + assert all(len(attempt.originir_sha256) == 64 for attempt in attempts) + assert all(attempt.compiled_two_qubit_gates == attempt.compiled_cnot_gates for attempt in attempts) + assert all(attempt.compiled_total_gates >= attempt.compiled_two_qubit_gates >= 0 for attempt in attempts) + assert all(attempt.compiled_depth >= 0 for attempt in attempts) + assert len({attempt.compiler_profile_fingerprint for attempt in attempts}) == 1 + + +def test_one_failed_repeat_is_retained_and_no_sixth_compile_is_attempted(): + compilation = compose_end_to_end_program(_compatible_build(), basis="walsh") + + class FlakyTranspiler: + calls = 0 + + def transpile(self, *args, **kwargs): + type(self).calls += 1 + if type(self).calls == 3: + raise RuntimeError("synthetic repeat failure") + return Transpiler().transpile(*args, **kwargs) + + attempts = compile_five_repetitions( + compilation, transpiler_factory=FlakyTranspiler + ) + audit = audit_build_resources( + _compatible_build(), + basis="walsh", + attempts=attempts, + ) + + assert FlakyTranspiler.calls == 5 + assert len(attempts) == 5 + assert sum(attempt.success for attempt in attempts) == 4 + assert attempts[2].status == "compile_failure" + assert attempts[2].exception_type == "RuntimeError" + assert audit.valid is False + assert audit.status == "compile_failure" + assert audit.successful_attempts == 4 + assert audit.failed_attempts == 1 + assert len(audit.compilation_attempts) == 5 + + +def test_compiler_internal_invariant_propagates(): + compilation = compose_end_to_end_program(_compatible_build(), basis="walsh") + + class BrokenInternalTranspiler: + def transpile(self, *args, **kwargs): + raise InternalInvariantError(code="synthetic_compiler_invariant") + + with pytest.raises(InternalInvariantError) as exc_info: + compile_five_repetitions( + compilation, transpiler_factory=BrokenInternalTranspiler + ) + assert exc_info.value.code == "synthetic_compiler_invariant" diff --git a/test/QAlgBase/test_qseencode_docs.py b/test/QAlgBase/test_qseencode_docs.py new file mode 100644 index 00000000..49bbdbc9 --- /dev/null +++ b/test/QAlgBase/test_qseencode_docs.py @@ -0,0 +1,71 @@ +import json +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +PACKAGE_DOCS = ( + REPOSITORY_ROOT + / "pyqpanda-algorithm" + / "pyqpanda_alg" + / "QSEncode" +) +NOTEBOOK = ( + REPOSITORY_ROOT + / "pyqpanda-algorithm" + / "example" + / "QAlgBase" + / "QSEncode_Insight_Demo.ipynb" +) + + +def test_qseencode_readme_has_required_user_sections_and_valid_links(): + readme = (PACKAGE_DOCS / "README.md").read_text(encoding="utf-8") + for section in ( + "# QSEncode-Insight", + "## Why it exists", + "## Source-checkout setup", + "## Quick start", + "## Standard and audit verification", + "## Evidence scope", + "## CLI", + "## Limitations", + ): + assert section in readme + assert (PACKAGE_DOCS / "BENCHMARK_EVIDENCE.md").is_file() + assert NOTEBOOK.is_file() + assert "no automatic Walsh/Fourier selector" in readme + assert "not a claim of quantum speedup" in readme + + +def test_benchmark_summary_retains_negative_evidence_and_claim_boundary(): + evidence = (PACKAGE_DOCS / "BENCHMARK_EVIDENCE.md").read_text(encoding="utf-8") + normalized = " ".join(evidence.split()) + assert "480 evaluation cells" in evidence + assert "45.27%" in evidence and "71.11%" in evidence + assert "55 `do_not_compress`" in evidence + assert "Dirichlet family was much weaker" in evidence + assert "do not establish quantum advantage" in normalized + + +def test_notebook_is_small_clean_and_all_code_cells_compile(): + notebook = json.loads(NOTEBOOK.read_text(encoding="utf-8")) + assert notebook["nbformat"] == 4 + assert NOTEBOOK.stat().st_size < 100_000 + assert len(notebook["cells"]) == 17 + source = "\n".join( + "".join(cell["source"]) for cell in notebook["cells"] + ) + for required in ( + "Walsh mode refuses compression", + "Fourier mode selects sparse preparation", + "Candidate table", + "Audit verification", + "EvidenceScope", + "## Exercise", + "## Limitations", + ): + assert required in source + for index, cell in enumerate(notebook["cells"]): + if cell["cell_type"] == "code": + assert cell["outputs"] == [] + compile("".join(cell["source"]), f"notebook-cell-{index}", "exec") diff --git a/test/QAlgBase/test_qseencode_facade.py b/test/QAlgBase/test_qseencode_facade.py new file mode 100644 index 00000000..b065a7da --- /dev/null +++ b/test/QAlgBase/test_qseencode_facade.py @@ -0,0 +1,195 @@ +import json +from dataclasses import replace + +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode import ( + EvidenceScopeStatus, + QSEncodeInsight, + ResultBindingError, + SelectionDecision, + SemanticVerification, + UncertifiedSelectionError, + VerificationLevel, +) + + +PROBABILITIES = np.array([ + 0.0006917643261373052, 0.015724004731018214, + 0.1261730210273901, 0.3574112099154543, + 0.3574112099154544, 0.1261730210273902, + 0.01572400473101823, 0.0006917643261373052, +]) + + +@pytest.fixture(scope="module") +def walsh_result(): + return QSEncodeInsight(basis="walsh").analyze(PROBABILITIES) + + +@pytest.fixture(scope="module") +def fourier_result(): + return QSEncodeInsight(basis="fourier").analyze(PROBABILITIES) + + +def test_walsh_public_example_is_complete_dnc_and_prepares_baseline(walsh_result): + engine = QSEncodeInsight(basis="walsh") + + assert walsh_result.selection.decision is SelectionDecision.DO_NOT_COMPRESS + assert walsh_result.transform is not None + assert walsh_result.error_budget is not None + assert walsh_result.capabilities + assert walsh_result.candidates + assert walsh_result.semantic_verification.status == "not_run_by_standard" + assert walsh_result.evidence_scope.status is EvidenceScopeStatus.VALIDATED_DEFAULT + artifact = engine.prepare(PROBABILITIES, result=walsh_result) + + assert artifact.selected_candidate_id == "dense_full__amplitude_encode" + assert artifact.decision is SelectionDecision.DO_NOT_COMPRESS + assert artifact.k is None + assert artifact.verification_status == "standard_validated" + + +def test_fourier_public_example_compresses_and_prepares_sparse_winner(fourier_result): + engine = QSEncodeInsight(basis="fourier") + + assert fourier_result.selection.decision is SelectionDecision.COMPRESS + assert fourier_result.selection.selected_candidate_id == "compressed__k4__sparse_isometry" + assert fourier_result.evidence_scope.status is EvidenceScopeStatus.VALIDATED_DEFAULT + artifact = engine.prepare(PROBABILITIES, result=fourier_result) + + assert artifact.selected_candidate_id == "compressed__k4__sparse_isometry" + assert artifact.decision is SelectionDecision.COMPRESS + assert artifact.k == 4 + assert artifact.output_qubits == (3, 4, 5) + + +def test_nondefault_target_runs_but_is_outside_validated_scope(): + result = QSEncodeInsight( + basis="walsh", fidelity_target=0.98 + ).analyze(PROBABILITIES) + + assert result.evidence_scope.status is EvidenceScopeStatus.OUTSIDE_VALIDATED_SCOPE + assert "fidelity_target_not_0.99" in result.evidence_scope.reasons + + +def test_prepare_binding_rejects_input_before_program_build(walsh_result, monkeypatch): + import pyqpanda_alg.QSEncode.insight as module + + monkeypatch.setattr( + module, + "build_preparation", + lambda *args, **kwargs: pytest.fail("binding must precede program build"), + ) + with pytest.raises(ResultBindingError) as error: + QSEncodeInsight(basis="walsh").prepare( + np.roll(PROBABILITIES, 1), result=walsh_result + ) + assert error.value.code == "input_mismatch" + + +def test_prepare_binding_rejects_configuration_mismatch(walsh_result): + with pytest.raises(ResultBindingError) as error: + QSEncodeInsight(basis="fourier").prepare( + PROBABILITIES, result=walsh_result + ) + assert error.value.code == "configuration_mismatch" + + +def test_audit_pass_has_exactly_five_certifications_and_prepares_winner(): + engine = QSEncodeInsight(basis="fourier", verification="audit") + result = engine.analyze(PROBABILITIES) + + assert result.semantic_verification.status == "certified_pass" + assert result.semantic_verification.recommendation_valid is True + assert len(result.semantic_verification.attempts) == 5 + artifact = engine.prepare(PROBABILITIES, result=result) + assert artifact.verification_status == "audit_certified_5_of_5" + + +def test_audit_failure_blocks_prepare_and_explicit_fallback_returns_baseline(monkeypatch): + import pyqpanda_alg.QSEncode.insight as module + + original = module.verify_resource_selection + + def invalidate(*args, **kwargs): + verified = original(*args, **kwargs) + return SemanticVerification( + level=VerificationLevel.AUDIT, + status="certified_fail", + recommendation_valid=False, + minimum_fidelity=0.5, + technical_repetitions=5, + selected_candidate_id=verified.selected_candidate_id, + attempts=verified.attempts, + ) + + monkeypatch.setattr(module, "verify_resource_selection", invalidate) + engine = QSEncodeInsight(basis="fourier", verification="audit") + result = engine.analyze(PROBABILITIES) + selected_before = result.selection + + with pytest.raises(UncertifiedSelectionError): + engine.prepare(PROBABILITIES, result=result) + artifact = engine.prepare( + PROBABILITIES, result=result, fallback_to_baseline=True + ) + + assert result.selection is selected_before + assert artifact.selected_candidate_id == "dense_full__amplitude_encode" + assert artifact.provenance["artifact_kind"] == "fallback_from_uncertified_selection" + assert artifact.provenance["original_selected_candidate_id"] == selected_before.selected_candidate_id + + +def test_result_nested_snapshots_are_immutable_and_json_stable(fourier_result): + with pytest.raises(TypeError): + fourier_result.analysis_config["basis"] = "walsh" + with pytest.raises(TypeError): + fourier_result.selection.comparison_metrics["depth_difference"] = 0 + candidate = next(item for item in fourier_result.candidates if item.resource_audit) + with pytest.raises(TypeError): + candidate.resource_audit.compiler_profile["topology"] = "all_to_all" + + first = fourier_result.to_json() + second = fourier_result.to_json() + payload = json.loads(first) + assert first == second + assert payload["selection"]["decision"] == "compress" + assert payload["semantic_verification"]["status"] == "not_run_by_standard" + assert "compiled_program" not in first + assert "compiled_originir" not in first + + +def test_result_defensively_snapshots_caller_owned_dicts_and_lists(fourier_result): + source_config = {"nested": {"items": [1, 2]}} + source_candidates = list(fourier_result.candidates) + snapshot = replace( + fourier_result, + analysis_config=source_config, + candidates=source_candidates, + ) + + source_config["nested"]["items"].append(3) + source_candidates.clear() + + assert snapshot.analysis_config["nested"]["items"] == (1, 2) + assert len(snapshot.candidates) == len(fourier_result.candidates) + with pytest.raises(TypeError): + snapshot.analysis_config["nested"]["new"] = "value" + + +def test_prepare_result_none_reuses_analyze(monkeypatch): + engine = QSEncodeInsight(basis="walsh") + calls = [] + original = engine.analyze + + def track(probabilities): + calls.append(1) + return original(probabilities) + + monkeypatch.setattr(engine, "analyze", track) + artifact = engine.prepare(PROBABILITIES) + + assert calls == [1] + assert artifact.decision is SelectionDecision.DO_NOT_COMPRESS diff --git a/test/QAlgBase/test_qseencode_insight_exceptions.py b/test/QAlgBase/test_qseencode_insight_exceptions.py new file mode 100644 index 00000000..39ec64a3 --- /dev/null +++ b/test/QAlgBase/test_qseencode_insight_exceptions.py @@ -0,0 +1,21 @@ +import pytest + +from pyqpanda_alg.QSEncode import ( + ConfigurationError, + QSEncodeInsightError, + ResultBindingError, +) + + +@pytest.mark.parametrize("code", ["input_mismatch", "configuration_mismatch"]) +def test_result_binding_error_has_stable_code_and_message(code): + error = ResultBindingError(code=code) + assert isinstance(error, QSEncodeInsightError) + assert error.code == code + assert str(error) == code + + +def test_base_exception_supports_explicit_message_without_losing_code(): + error = ConfigurationError(code="invalid_basis", message="basis is invalid") + assert error.code == "invalid_basis" + assert str(error) == "basis is invalid" diff --git a/test/QAlgBase/test_qseencode_insight_fingerprint.py b/test/QAlgBase/test_qseencode_insight_fingerprint.py new file mode 100644 index 00000000..39363986 --- /dev/null +++ b/test/QAlgBase/test_qseencode_insight_fingerprint.py @@ -0,0 +1,120 @@ +from dataclasses import replace + +from pyqpanda_alg.QSEncode import ( + AnalysisConfig, + CompilerConfig, + EvidenceScopeStatus, + InputPolicy, + PreparationMethod, + SelectionDecision, + VerificationLevel, + analysis_config_fingerprint, + canonical_analysis_config_json, +) + + +DEFAULT_WALSH_CANONICAL_JSON = ( + '{"basis":"walsh","compiler":{"basis_gates":["U3","CNOT"],' + '"initial_mapping":"identity","optimization_level":2,' + '"physical_capacity_multiplier":2,"pyqpanda_version":"0.3.5",' + '"resource_aggregation":"median_with_range","technical_repetitions":5,' + '"topology":"linear"},"fidelity_target":"0x1.fae147ae147aep-1",' + '"input_policy":{"normalization":"normalize",' + '"normalization_tolerance":"0x1.19799812dea11p-40",' + '"padding":"next_power_of_two"},"methods":["amplitude_encode",' + '"sparse_isometry","ds_quantum_state_preparation"],' + '"schema_version":"qseencode-insight-v1",' + '"selection_policy":"frozen_lexicographic_v1",' + '"verification":"standard"}' +) +DEFAULT_WALSH_FINGERPRINT = ( + "6530A50A213BB2C74FF71D0C9A0D36BEA84F4A3CE1EB0E6A94CDDE00C0B9FD39" +) + + +def make_config(**changes): + base = AnalysisConfig( + basis="walsh", + fidelity_target=0.99, + verification=VerificationLevel.STANDARD, + ) + return replace(base, **changes) + + +def test_mapping_key_insertion_order_does_not_change_fingerprint(): + first = {"basis": "walsh", "nested": {"b": 2, "a": 1}} + second = {"nested": {"a": 1, "b": 2}, "basis": "walsh"} + assert analysis_config_fingerprint(first) == analysis_config_fingerprint(second) + + +def test_default_walsh_config_has_absolute_canonical_json_and_hash_golden(): + config = make_config() + assert canonical_analysis_config_json(config) == DEFAULT_WALSH_CANONICAL_JSON + assert analysis_config_fingerprint(config).upper() == DEFAULT_WALSH_FINGERPRINT + + +def test_each_frozen_selection_configuration_dimension_changes_fingerprint(): + baseline = make_config() + baseline_hash = analysis_config_fingerprint(baseline) + variants = ( + replace( + baseline, + methods=( + PreparationMethod.SPARSE_ISOMETRY, + PreparationMethod.AMPLITUDE_ENCODE, + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + ), + ), + replace(baseline, basis="fourier"), + replace(baseline, fidelity_target=0.98), + replace( + baseline, + compiler=replace(CompilerConfig(), optimization_level=1), + ), + replace( + baseline, + input_policy=replace(InputPolicy(), padding="reject"), + ), + replace(baseline, verification=VerificationLevel.AUDIT), + ) + assert all( + analysis_config_fingerprint(variant) != baseline_hash for variant in variants + ) + + +def test_fingerprint_is_deterministic_and_uses_float_hex_canonicalization(): + config = make_config() + assert analysis_config_fingerprint(config) == analysis_config_fingerprint(config) + assert config.canonical_payload()["fidelity_target"] == float(0.99).hex() + + +def test_public_enum_values_and_compiler_profile_defaults_are_frozen(): + assert tuple(item.value for item in VerificationLevel) == ("standard", "audit") + assert tuple(item.value for item in EvidenceScopeStatus) == ( + "validated_default", + "outside_validated_scope", + ) + assert tuple(item.value for item in SelectionDecision) == ( + "compress", + "do_not_compress", + ) + compiler = CompilerConfig() + assert ( + compiler.pyqpanda_version, + compiler.topology, + compiler.physical_capacity_multiplier, + compiler.initial_mapping, + compiler.optimization_level, + compiler.basis_gates, + compiler.technical_repetitions, + compiler.resource_aggregation, + ) == ( + "0.3.5", + "linear", + 2, + "identity", + 2, + ("U3", "CNOT"), + 5, + "median_with_range", + ) diff --git a/test/QAlgBase/test_qseencode_insight_models.py b/test/QAlgBase/test_qseencode_insight_models.py new file mode 100644 index 00000000..4798e7dc --- /dev/null +++ b/test/QAlgBase/test_qseencode_insight_models.py @@ -0,0 +1,105 @@ +from dataclasses import FrozenInstanceError, fields + +import pytest + +from pyqpanda_alg.QSEncode import ( + AttributionReport, + CandidateResult, + CapabilityReport, + ErrorBudgetResult, + EvidenceScope, + EvidenceScopeStatus, + InputSummary, + InsightResult, + PreparationArtifact, + ResourceAudit, + SelectionResult, + SemanticVerification, + SemanticVerificationAttempt, + TransformDiagnostics, +) + + +MODEL_TYPES = ( + InputSummary, + TransformDiagnostics, + ErrorBudgetResult, + CapabilityReport, + CandidateResult, + SelectionResult, + ResourceAudit, + SemanticVerification, + SemanticVerificationAttempt, + AttributionReport, + EvidenceScope, + InsightResult, + PreparationArtifact, +) + + +@pytest.mark.parametrize("model_type", MODEL_TYPES) +def test_all_schema_models_use_frozen_slotted_dataclasses(model_type): + assert model_type.__dataclass_params__.frozen is True + assert "__slots__" in model_type.__dict__ + + +def test_input_summary_is_frozen_slotted_and_has_required_hash_fields(): + names = {field.name for field in fields(InputSummary)} + assert { + "original_length", + "padded_length", + "original_sum", + "normalized", + "padding_count", + "original_input_sha256", + "effective_probability_sha256", + } <= names + summary = InputSummary( + original_length=2, + padded_length=2, + original_sum=1.0, + normalized=False, + padding_count=0, + original_input_sha256="a" * 64, + effective_probability_sha256="b" * 64, + ) + assert not hasattr(summary, "__dict__") + with pytest.raises(FrozenInstanceError): + summary.original_length = 3 + + +def test_insight_result_schema_version_and_config_provenance_are_contract_fields(): + names = {field.name for field in fields(InsightResult)} + assert "schema_version" in names + assert "analysis_config_fingerprint" in names + assert "analysis_config" in names + assert InsightResult.SCHEMA_VERSION == "qseencode-insight-v1" + + +def test_evidence_scope_uses_stable_status_enum(): + scope = EvidenceScope( + status=EvidenceScopeStatus.OUTSIDE_VALIDATED_SCOPE, + reasons=("phase1_contract_only",), + reference=None, + ) + assert scope.status.value == "outside_validated_scope" + assert scope.reasons == ("phase1_contract_only",) + + +def test_phase3_capability_report_reserves_explanatory_and_register_metadata(): + names = {field.name for field in fields(CapabilityReport)} + assert { + "method", + "compatible", + "reason_code", + "reason", + "required_qubits", + "ancillas", + "input_constraints", + "observed_output_qubits", + "exception_type", + "exception_message", + "diagnostics", + "failure_stage", + "logical_fidelity", + } <= names diff --git a/test/QAlgBase/test_qseencode_insight_public_contract.py b/test/QAlgBase/test_qseencode_insight_public_contract.py new file mode 100644 index 00000000..384ff0f9 --- /dev/null +++ b/test/QAlgBase/test_qseencode_insight_public_contract.py @@ -0,0 +1,82 @@ +import inspect + +import pytest + +import pyqpanda_alg.QSEncode as public_module +from pyqpanda_alg.QSEncode import ( + ConfigurationError, + DEFAULT_METHODS, + InsightResult, + PreparationMethod, + QSEncodeInsight, +) + + +def test_basis_is_a_required_keyword_only_argument(): + signature = inspect.signature(QSEncodeInsight) + assert tuple(signature.parameters) == ( + "basis", + "fidelity_target", + "verification", + "methods", + "compiler", + "input_policy", + ) + assert signature.parameters["basis"].kind is inspect.Parameter.KEYWORD_ONLY + assert signature.parameters["basis"].default is inspect.Parameter.empty + + with pytest.raises(TypeError, match="basis"): + QSEncodeInsight() + + +@pytest.mark.parametrize("basis", ["walsh", "fourier"]) +def test_explicit_valid_basis_constructs_contract_facade(basis): + engine = QSEncodeInsight(basis=basis) + assert engine.basis == basis + assert engine.fidelity_target == 0.99 + assert engine.methods == DEFAULT_METHODS + + +@pytest.mark.parametrize("basis", [None, "auto", "WALSH", ""]) +def test_invalid_basis_is_rejected_without_fallback(basis): + with pytest.raises(ConfigurationError) as exc_info: + QSEncodeInsight(basis=basis) + assert getattr(exc_info.value, "code", None) == "invalid_basis" + + +def test_default_method_enum_order_and_values_are_stable(): + assert DEFAULT_METHODS == ( + PreparationMethod.AMPLITUDE_ENCODE, + PreparationMethod.SPARSE_ISOMETRY, + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + ) + assert tuple(method.value for method in DEFAULT_METHODS) == ( + "amplitude_encode", + "sparse_isometry", + "ds_quantum_state_preparation", + ) + + +def test_phase7_analyze_and_prepare_are_activated_product_apis(): + engine = QSEncodeInsight(basis="walsh") + result = engine.analyze([0.5, 0.5]) + artifact = engine.prepare([0.5, 0.5], result=result) + assert result.selection is not None + assert artifact.program is not None + + +def test_public_result_type_is_exported_without_constructing_fake_result(): + assert InsightResult.__name__ == "InsightResult" + + +def test_public_all_keeps_legacy_and_adds_phase1_contract_names(): + required = { + "QSpare_Code", + "QSEncodeInsight", + "InsightResult", + "PreparationMethod", + "ResultBindingError", + "analysis_config_fingerprint", + } + assert required <= set(public_module.__all__) + assert all(hasattr(public_module, name) for name in public_module.__all__) diff --git a/test/QAlgBase/test_qseencode_legacy_characterization.py b/test/QAlgBase/test_qseencode_legacy_characterization.py new file mode 100644 index 00000000..45f135d3 --- /dev/null +++ b/test/QAlgBase/test_qseencode_legacy_characterization.py @@ -0,0 +1,172 @@ +"""Characterization tests for the frozen QSpare_Code legacy API. + +Expected constants were extracted read-only from +upstream/develop@5f973efccb84bc193157d1ccebe32137e307293b on 2026-08-21. +These tests intentionally preserve observable quirks; they are not a preferred +contract for the new QSEncodeInsight facade. +""" + +import inspect + +import numpy as np +import pytest +from pyqpanda3.core import QCircuit, QProg + +from pyqpanda_alg.QSEncode import QSpare_Code + + +def test_legacy_import_and_constructor_signature_are_unchanged(): + assert QSpare_Code.__name__ == "QSpare_Code" + assert str(inspect.signature(QSpare_Code)) == ( + "(prob_list=None, cut_length=None, mode='walsh')" + ) + + +def test_constructor_positional_keyword_defaults_and_observable_attributes(): + defaulted = QSpare_Code([0.25, 0.75]) + positional = QSpare_Code([0.25, 0.75], 1, "fourier") + keyword = QSpare_Code(prob_list=[0.25, 0.75], cut_length=1, mode="fourier") + + assert defaulted.cut == 2 * defaulted.qubits_num == 2 + assert defaulted.mode == "walsh" + assert positional.cut == keyword.cut == 1 + assert positional.mode == keyword.mode == "fourier" + np.testing.assert_allclose(defaulted.prob, [0.25, 0.75]) + np.testing.assert_allclose(defaulted.amp, [0.5, np.sqrt(0.75)]) + + +@pytest.mark.parametrize( + "probabilities", + [ + [0.25, 0.75], + np.array([0.25, 0.75], dtype=np.float64), + [np.float64(0.25), np.float64(0.75)], + ], +) +def test_constructor_accepts_current_float_containers(probabilities): + assert isinstance(QSpare_Code(probabilities), QSpare_Code) + + +@pytest.mark.parametrize( + ("probabilities", "message"), + [ + (None, "prob list should be supported"), + ((0.25, 0.75), "prob_list should be np.ndarray or list"), + ([], "at least one number in the prob_list "), + ([0, 1], "elements of prob_list should be float type"), + ( + np.array([np.float32(0.25), np.float32(0.75)]), + "elements of prob_list should be float type", + ), + ([-0.1, 1.1], "prob must > 0"), + ], +) +def test_constructor_rejects_inputs_with_exact_legacy_errors(probabilities, message): + with pytest.raises(ValueError, match=f"^{message}$"): + QSpare_Code(probabilities) + + +def test_sum_tolerance_raises_warning_object_instead_of_emitting_warning(): + accepted = QSpare_Code([0.5, 0.5009]) + np.testing.assert_allclose(accepted.prob.sum(), 1.0) + + with pytest.raises(Warning, match="sum of prob list should be 1"): + QSpare_Code([0.5, 0.5011]) + + +def test_nan_currently_passes_constructor_and_remains_nan(): + result = QSpare_Code([float("nan"), 0.0]) + assert np.isnan(result.prob).all() + assert np.isnan(result.amp).all() + + +def test_power_of_two_padding_qubits_and_default_cut_are_preserved(): + result = QSpare_Code([0.2, 0.3, 0.5]) + assert result.qubits_num == 2 + assert len(result.prob) == 4 + assert result.cut == 4 + np.testing.assert_allclose(result.prob, [0.2, 0.3, 0.5, 0.0]) + np.testing.assert_allclose(result.amp, np.sqrt([0.2, 0.3, 0.5, 0.0])) + + +def test_single_value_and_unvalidated_constructor_fields_are_preserved(): + single = QSpare_Code([1.0]) + assert single.qubits_num == 0 + assert single.cut == 0 + + assert QSpare_Code([0.5, 0.5], cut_length=0).cut == 0 + assert QSpare_Code([0.5, 0.5], cut_length=1.0).cut == 1.0 + assert QSpare_Code([0.5, 0.5], cut_length=99).cut == 99 + assert QSpare_Code([0.5, 0.5], mode="invalid").mode == "invalid" + + +def test_select_top_n_return_type_ties_and_boundary_validation(): + engine = QSpare_Code([0.25, 0.75]) + values = np.array([1 + 1j, -3j, 2 + 0j]) + + selected = engine.select_top_n_complex_numbers(values, 2) + assert isinstance(selected, np.ndarray) + assert selected.dtype == np.complex128 + np.testing.assert_array_equal(selected, [0j, -3j, 2 + 0j]) + np.testing.assert_array_equal( + engine.select_top_n_complex_numbers(values, 99), values + ) + + for invalid in (0, -1, 1.0, np.int64(1)): + with pytest.raises(ValueError, match="n must > 0 and with class int"): + engine.select_top_n_complex_numbers(values, invalid) + + +@pytest.mark.parametrize( + ("mode", "expected"), + [ + ( + "walsh", + [ + 0.9718097255278189, + -0.1078594020058149, + -0.208368364011023, + -0.02312642747730509, + ], + ), + ( + "fourier", + [ + 1.943619451055638 + 0j, + -0.23149479148832813 + 0.18524193653371795j, + -0.2157188040116298 + 0j, + -0.23149479148832813 - 0.18524193653371795j, + ], + ), + ], +) +def test_transform_small_n_convention_ordering_and_complex_behavior(mode, expected): + engine = QSpare_Code([0.1, 0.2, 0.3, 0.4], mode=mode) + transformed = engine.Transform(engine.amp) + assert isinstance(transformed, np.ndarray) + np.testing.assert_allclose( + np.asarray(transformed, dtype=np.complex128), expected, atol=1e-14 + ) + if mode == "walsh": + assert transformed.dtype == object + else: + assert transformed.dtype == np.complex128 + + +def test_invalid_transform_mode_is_rejected_when_transform_is_called(): + engine = QSpare_Code([0.25, 0.75], mode="invalid") + with pytest.raises(ValueError, match="mode only support walsh or fourier"): + engine.Transform(engine.amp) + + +@pytest.mark.parametrize("mode", ["walsh", "fourier"]) +def test_quantum_circuit_and_minimal_quantum_result_contract(mode): + engine = QSpare_Code([0.25, 0.75], cut_length=1, mode=mode) + qubits = QProg(engine.qubits_num).qubits() + circuit = engine.quantum_cir(qubits) + result = engine.Quantum_Res() + + assert isinstance(circuit, QCircuit) + assert isinstance(result, list) + assert len(result) == 2 + np.testing.assert_allclose(result, [0.5, 0.5], atol=1e-12) diff --git a/test/QAlgBase/test_qseencode_preparation_adapters.py b/test/QAlgBase/test_qseencode_preparation_adapters.py new file mode 100644 index 00000000..ffb9cc1c --- /dev/null +++ b/test/QAlgBase/test_qseencode_preparation_adapters.py @@ -0,0 +1,68 @@ +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode import PreparationMethod +from pyqpanda_alg.QSEncode._preparation import ( + _normalize_backend_output_register, + adapt_preparation_input, + build_preparation, +) + + +@pytest.mark.parametrize( + "method", + [ + PreparationMethod.AMPLITUDE_ENCODE, + PreparationMethod.SPARSE_ISOMETRY, + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + ], +) +def test_all_three_adapters_construct_program_and_metadata(method): + selected = np.array([1 / np.sqrt(2), 0, 0, 1j / np.sqrt(2)]) + prepared_input = adapt_preparation_input(selected) + + build = build_preparation(method, prepared_input) + + assert build.status == "success" + assert build.program is not None + assert build.circuit is not None + assert len(build.output_qubits) == 2 + assert len(set(build.output_qubits)) == 2 + assert set(build.output_qubits).isdisjoint(build.ancillas) + assert build.required_qubits == len(build.output_qubits) + len(build.ancillas) + assert build.logical_output_qubits == 2 + + +def test_method_specific_qubit_and_representation_contracts(): + selected = adapt_preparation_input([1.0, 0.0, 0.0, 0.0]) + amplitude = build_preparation(PreparationMethod.AMPLITUDE_ENCODE, selected) + sparse = build_preparation(PreparationMethod.SPARSE_ISOMETRY, selected) + ds = build_preparation(PreparationMethod.DS_QUANTUM_STATE_PREPARATION, selected) + + assert (amplitude.required_qubits, amplitude.ancillas, amplitude.input_representation) == (2, (), "dense_list") + assert (sparse.required_qubits, sparse.ancillas, sparse.input_representation) == (2, (), "sparse_binary_map") + assert ds.required_qubits == 4 + assert len(ds.ancillas) == 2 + assert ds.input_representation == "sparse_binary_map" + + +def test_complex_amplitude_backend_duplicate_output_metadata_is_normalized_and_recorded(): + selected = adapt_preparation_input([1 / np.sqrt(2), 0, 1j / np.sqrt(2), 0]) + build = build_preparation(PreparationMethod.AMPLITUDE_ENCODE, selected) + + assert build.output_qubits == (0, 1) + assert build.diagnostics["backend_reported_output_qubits"] == (0, 1, 0, 1) + assert build.diagnostics["output_register_normalization"] == "deduplicated_exact_repetition" + + +def test_real_amplitude_duplicate_output_metadata_is_not_normalized(): + output, normalization = _normalize_backend_output_register( + PreparationMethod.AMPLITUDE_ENCODE, + (0, 1), + (0, 1, 0, 1), + 2, + is_complex=False, + ) + + assert output == (0, 1, 0, 1) + assert normalization is None diff --git a/test/QAlgBase/test_qseencode_preparation_correctness.py b/test/QAlgBase/test_qseencode_preparation_correctness.py new file mode 100644 index 00000000..93d81cb3 --- /dev/null +++ b/test/QAlgBase/test_qseencode_preparation_correctness.py @@ -0,0 +1,38 @@ +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode import PreparationMethod +from pyqpanda_alg.QSEncode._capability import assess_capability +from pyqpanda_alg.QSEncode._preparation import adapt_preparation_input + + +FIXTURES = ( + np.array([1.0, 0.0, 0.0, 0.0]), + np.array([1 / np.sqrt(2), 0.0, 0.0, 1 / np.sqrt(2)]), + np.array([1 / np.sqrt(2), 0.0, 0.0, 1j / np.sqrt(2)]), + np.array([1 / np.sqrt(3), 0, 0, 1j / np.sqrt(3), 0, -1 / np.sqrt(3), 0, 0]), +) + + +@pytest.mark.parametrize("method", list(PreparationMethod)) +@pytest.mark.parametrize("state", FIXTURES) +def test_small_n_logical_state_fidelity_is_global_phase_safe(method, state): + assessment = assess_capability(method, adapt_preparation_input(state)) + + assert assessment.report.compatible is True + assert assessment.report.reason_code == "compatible" + assert assessment.report.logical_fidelity >= 1.0 - 1e-10 + assert assessment.report.failure_stage is None + + +def test_ds_reports_output_and_ancilla_semantics_from_actual_build(): + assessment = assess_capability( + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + adapt_preparation_input([1 / np.sqrt(2), 0, 0, 1j / np.sqrt(2)]), + ) + report = assessment.report + + assert report.observed_output_qubits == (2, 3) + assert report.ancillas == (0, 1) + assert report.required_qubits == 4 + assert set(report.observed_output_qubits).isdisjoint(report.ancillas) diff --git a/test/QAlgBase/test_qseencode_preparation_data.py b/test/QAlgBase/test_qseencode_preparation_data.py new file mode 100644 index 00000000..f0acd3d2 --- /dev/null +++ b/test/QAlgBase/test_qseencode_preparation_data.py @@ -0,0 +1,49 @@ +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode._preparation import ( + SPARSE_SUPPORT_THRESHOLD, + adapt_preparation_input, +) +from pyqpanda_alg.QSEncode.exceptions import InputValidationError + + +def test_dense_adapter_preserves_complex_values_and_caller_input(): + source = np.array([1 / np.sqrt(2), 0, 1j / np.sqrt(2), 0]) + original = source.copy() + + adapted = adapt_preparation_input(source) + + np.testing.assert_array_equal(source, original) + np.testing.assert_array_equal(adapted.coefficients, original) + assert np.iscomplexobj(adapted.coefficients) + assert adapted.coefficients.flags.writeable is False + assert adapted.logical_dimension == 4 + assert adapted.logical_output_qubits == 2 + + +def test_sparse_adapter_uses_frozen_strict_representation_cleanup_threshold(): + tiny = SPARSE_SUPPORT_THRESHOLD + source = np.array([np.sqrt(1.0 - (2 * tiny) ** 2), tiny, 2 * tiny, 0.0]) + source /= np.linalg.norm(source) + + adapted = adapt_preparation_input(source) + + assert adapted.support_indices == (0, 2) + assert tuple(key for key, _ in adapted.sparse_items) == ("00", "10") + assert adapted.support_threshold == 1e-14 + + +@pytest.mark.parametrize( + ("values", "code"), + [ + ([1.0, 0.0, 0.0], "invalid_state_dimension"), + ([1.0, 1.0, 0.0, 0.0], "state_not_normalized"), + ([0.0, 0.0, 0.0, 0.0], "state_not_normalized"), + ([np.nan, 0.0, 0.0, 0.0], "nonfinite_state"), + ], +) +def test_adapter_rejects_invalid_selected_states(values, code): + with pytest.raises(InputValidationError) as exc_info: + adapt_preparation_input(values) + assert exc_info.value.code == code diff --git a/test/QAlgBase/test_qseencode_preparation_golden_parity.py b/test/QAlgBase/test_qseencode_preparation_golden_parity.py new file mode 100644 index 00000000..90d90ff6 --- /dev/null +++ b/test/QAlgBase/test_qseencode_preparation_golden_parity.py @@ -0,0 +1,44 @@ +"""Copied constants only; Frozen evidence is never imported at runtime. + +Provenance: +- phase3a_preflight.py SHA-256 + B7252BF0D52E22250AB6BB973EED9EA366ED2D3ADB8B0D435DDB49CADDF9641B +- final_technical_hardening.py SHA-256 + B24336428BD62DFF3C479B5C1207329BC61AA68B92009E77A2EE6537981D11A8 +- case semantics: small-N sparse_isometry and DS compatible state preparation, + output-register-aware logical fidelity. +""" + +import numpy as np + +from pyqpanda_alg.QSEncode import PreparationMethod +from pyqpanda_alg.QSEncode._capability import assess_capability +from pyqpanda_alg.QSEncode._preparation import adapt_preparation_input + + +FROZEN_COMPATIBLE_STATE = np.array( + [1 / np.sqrt(3), 0, 0, 1j / np.sqrt(3), 0, -1 / np.sqrt(3), 0, 0] +) + + +def test_frozen_sparse_isometry_compatible_case_semantics(): + assessment = assess_capability( + PreparationMethod.SPARSE_ISOMETRY, + adapt_preparation_input(FROZEN_COMPATIBLE_STATE), + ) + assert assessment.report.compatible + assert assessment.report.observed_output_qubits == (0, 1, 2) + assert assessment.report.ancillas == () + assert assessment.report.logical_fidelity >= 1.0 - 1e-10 + + +def test_frozen_ds_compatible_case_semantics(): + assessment = assess_capability( + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + adapt_preparation_input(FROZEN_COMPATIBLE_STATE), + ) + assert assessment.report.compatible + assert assessment.report.observed_output_qubits == (3, 4, 5) + assert assessment.report.ancillas == (0, 1, 2) + assert assessment.report.required_qubits == 6 + assert assessment.report.logical_fidelity >= 1.0 - 1e-10 diff --git a/test/QAlgBase/test_qseencode_pure_error_budget.py b/test/QAlgBase/test_qseencode_pure_error_budget.py new file mode 100644 index 00000000..ae785112 --- /dev/null +++ b/test/QAlgBase/test_qseencode_pure_error_budget.py @@ -0,0 +1,87 @@ +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode import InputValidationError +from pyqpanda_alg.QSEncode._error_budget import ( + candidate_neighborhood, + find_k_star, + retained_energy_ratio, + stable_magnitude_order, + top_k_coefficients, + top_k_indices, +) + + +def test_frozen_stable_v1_exact_ties_keep_stable_ascending_order_then_suffix(): + coefficients = np.array([1.0, -1.0, 1.0, -1.0]) + np.testing.assert_array_equal(stable_magnitude_order(coefficients), [0, 1, 2, 3]) + np.testing.assert_array_equal(top_k_indices(coefficients, 2), [2, 3]) + np.testing.assert_array_equal( + top_k_coefficients(coefficients, 2), [0.0, 0.0, 1.0, -1.0] + ) + + +def test_normalized_top_k_state_preserves_selected_complex_coefficients(): + coefficients = np.array([1 + 1j, 4j, -2.0, 0.5]) + selected = top_k_coefficients(coefficients, 2, normalize=True) + expected = np.array([0j, 4j, -2.0 + 0j, 0j]) / np.sqrt(20.0) + np.testing.assert_allclose(selected, expected, atol=1e-12, rtol=1e-10) + + +def test_retained_energy_uses_shared_ranking_order(): + coefficients = np.array([1.0, 2.0, 3.0]) + assert retained_energy_ratio(coefficients, 1) == pytest.approx(9 / 14) + assert retained_energy_ratio(coefficients, 2) == pytest.approx(13 / 14) + assert retained_energy_ratio(coefficients, 3) == pytest.approx(1.0) + + +def test_k_star_minimality_middle_boundary(): + coefficients = np.sqrt(np.array([0.6, 0.3, 0.1])) + result = find_k_star(coefficients, 0.9) + assert result.k_star == 2 + assert result.previous_retained_energy == pytest.approx(0.6) + assert result.retained_energy == pytest.approx(0.9) + assert result.minimality_pass is True + assert result.candidate_k == (1, 2) + assert result.ranking_policy == "frozen_stable_v1" + + +def test_k_star_one_and_target_one_reaching_n(): + dominant = find_k_star(np.sqrt([0.995, 0.005]), 0.99) + assert dominant.k_star == 1 + assert dominant.previous_retained_energy == 0.0 + assert dominant.candidate_k == (1,) + + full = find_k_star(np.ones(4), 1.0) + assert full.k_star == 4 + assert full.retained_energy == pytest.approx(1.0) + assert full.candidate_k == (3,) + + +@pytest.mark.parametrize( + ("k_star", "size", "expected"), + [(1, 2, (1,)), (1, 8, (1, 2)), (4, 8, (3, 4, 5)), (8, 8, (7,))], +) +def test_candidate_neighborhood_exact_boundaries(k_star, size, expected): + assert candidate_neighborhood(k_star, size) == expected + + +@pytest.mark.parametrize( + ("coefficients", "code"), + [ + ([0.0, 0.0], "zero_coefficient_energy"), + ([1.0, np.nan], "nonfinite_coefficients"), + ([1.0, np.inf], "nonfinite_coefficients"), + ], +) +def test_invalid_coefficient_energy_has_exact_codes(coefficients, code): + with pytest.raises(InputValidationError) as exc_info: + find_k_star(coefficients, 0.99) + assert exc_info.value.code == code + + +@pytest.mark.parametrize("target", [0.0, -0.1, 1.1, np.nan, np.inf]) +def test_invalid_fidelity_target_is_rejected(target): + with pytest.raises(InputValidationError) as exc_info: + find_k_star([1.0, 1.0], target) + assert exc_info.value.code == "invalid_fidelity_target" diff --git a/test/QAlgBase/test_qseencode_pure_golden_parity.py b/test/QAlgBase/test_qseencode_pure_golden_parity.py new file mode 100644 index 00000000..752eee68 --- /dev/null +++ b/test/QAlgBase/test_qseencode_pure_golden_parity.py @@ -0,0 +1,90 @@ +"""Small Frozen scientific parity fixture. + +Provenance (constants copied; no runtime import): +- source: qseencode-final-technical-hardening-2026-08-18/ + final_technical_hardening.py +- source SHA-256: + B24336428BD62DFF3C479B5C1207329BC61AA68B92009E77A2EE6537981D11A8 +- result source: kstar_independent_recomputation.csv +- result SHA-256: + C71AEBAB8E5C8EB11D31727AA76DBCB42DA0D77533BFAEBCE394E5B35AC24F69 +- case identity: gaussian, N=8, fidelity_target=0.99, walsh/fourier +""" + +import numpy as np + +from pyqpanda_alg.QSEncode._error_budget import ( + find_k_star, + top_k_coefficients, + top_k_indices, +) +from pyqpanda_alg.QSEncode._transforms import normalized_fourier, normalized_fwht + + +PROBABILITIES = np.array( + [ + 0.0006917643261373052, + 0.015724004731018214, + 0.1261730210273901, + 0.3574112099154543, + 0.3574112099154544, + 0.1261730210273902, + 0.01572400473101823, + 0.0006917643261373052, + ] +) +FROZEN_WALSH = np.array( + [ + 0.7811719797235759, + 0.0, + 7.850462293418875e-17, + 0.10149554964818927, + -1.1775693440128312e-16, + -0.2416356009244349, + -0.566640298480657, + 3.925231146709437e-17, + ] +) +FROZEN_FOURIER_UNNORMALIZED = np.array( + [ + 2.209488016541843 + 0j, + -1.1381776680196536 - 0.47144862648392233j, + 0.1435363828329812 + 0.14353638283298098j, + -0.004897071058339253 - 0.011822575364947241j, + 0.0 + 0j, + -0.004897071058339253 + 0.011822575364947241j, + 0.1435363828329812 - 0.14353638283298098j, + -1.1381776680196536 + 0.47144862648392233j, + ] +) + + +def test_frozen_gaussian_walsh_coefficients_ranking_energy_and_kstar(): + coefficients = normalized_fwht(np.sqrt(PROBABILITIES)) + np.testing.assert_allclose(coefficients, FROZEN_WALSH, atol=1e-12, rtol=1e-10) + result = find_k_star(coefficients, 0.99) + assert result.k_star == 4 + np.testing.assert_array_equal(top_k_indices(coefficients, 4), [3, 5, 6, 0]) + assert abs(result.previous_retained_energy - 0.989698653401612) <= 1e-12 + assert abs(result.retained_energy - 1.0) <= 1e-12 + + +def test_frozen_gaussian_fourier_scale_scientific_invariants_and_kstar(): + coefficients = normalized_fourier(np.sqrt(PROBABILITIES)) + np.testing.assert_allclose( + coefficients * np.sqrt(8), + FROZEN_FOURIER_UNNORMALIZED, + atol=1e-12, + rtol=1e-10, + ) + result = find_k_star(coefficients, 0.99) + assert result.k_star == 4 + np.testing.assert_array_equal(top_k_indices(coefficients, 4), [6, 1, 7, 0]) + assert abs(result.previous_retained_energy - 0.9896577147533093) <= 1e-12 + assert abs(result.retained_energy - 0.9948083880525034) <= 1e-12 + np.testing.assert_allclose( + top_k_coefficients(coefficients, 4, normalize=True), + top_k_coefficients(FROZEN_FOURIER_UNNORMALIZED, 4, normalize=True), + atol=1e-12, + rtol=1e-10, + ) diff --git a/test/QAlgBase/test_qseencode_pure_hashing.py b/test/QAlgBase/test_qseencode_pure_hashing.py new file mode 100644 index 00000000..93aba6be --- /dev/null +++ b/test/QAlgBase/test_qseencode_pure_hashing.py @@ -0,0 +1,41 @@ +import numpy as np + +from pyqpanda_alg.QSEncode._validation import canonical_probability_sha256 + + +def test_original_input_hash_known_vector(): + assert canonical_probability_sha256( + np.array([1.0, 3.0, 0.0]), + domain=b"qseencode-original-input-v1\0", + ) == "e729a6fb50de27cc3f1868f7a7ccd7c322e735dcb64680ca127852e61e11c797" + + +def test_effective_probability_hash_known_vector(): + assert canonical_probability_sha256( + np.array([0.25, 0.75, 0.0, 0.0]), + domain=b"qseencode-effective-probability-v1\0", + ) == "2b120f880bd1947febe7ca47d2023eb587a39478584caadae547e43ecbacbf44" + + +def test_hash_is_independent_of_native_dtype_and_memory_layout(): + contiguous = np.array([0.2, 0.3, 0.5], dtype=" medians[PreparationMethod.AMPLITUDE_ENCODE] + assert medians[PreparationMethod.DS_QUANTUM_STATE_PREPARATION] > medians[PreparationMethod.SPARSE_ISOMETRY] + historical = { + PreparationMethod.AMPLITUDE_ENCODE: 21.0, + PreparationMethod.SPARSE_ISOMETRY: 22.0, + PreparationMethod.DS_QUANTUM_STATE_PREPARATION: 116.0, + } + for method, value in medians.items(): + assert 0.5 <= value / historical[method] <= 2.0 + + +def test_valid_resource_audit_contains_medians_ranges_and_width_metrics(): + audit = _audit([1.0, 0.0, 0.0, 0.0], PreparationMethod.AMPLITUDE_ENCODE) + + assert audit.valid and audit.status == "valid" + assert audit.successful_attempts == 5 and audit.failed_attempts == 0 + assert audit.two_qubit_range[0] <= audit.compiled_two_qubit_gates <= audit.two_qubit_range[1] + assert audit.depth_range[0] <= audit.compiled_depth <= audit.depth_range[1] + assert audit.q_required_times_depth == audit.required_qubits * audit.compiled_depth + assert audit.q_allocated_times_depth == audit.allocated_qubits * audit.compiled_depth + assert audit.compiled_two_qubit_gates == audit.compiled_cnot_gates diff --git a/test/QAlgBase/test_qseencode_sealed_regression.py b/test/QAlgBase/test_qseencode_sealed_regression.py new file mode 100644 index 00000000..252015f0 --- /dev/null +++ b/test/QAlgBase/test_qseencode_sealed_regression.py @@ -0,0 +1,148 @@ +"""Small sealed parity suite for the contest submission. + +The fixture is the preregistered Gaussian N=8 case. It does not read or run +the 480-cell Locked benchmark. +""" + +import numpy as np +import pytest + +from pyqpanda_alg.QSEncode import ( + EvidenceScopeStatus, + PreparationMethod, + QSEncodeInsight, + ResultBindingError, + SelectionDecision, +) +from pyqpanda_alg.QSEncode._capability import assess_capability +from pyqpanda_alg.QSEncode._preparation import adapt_preparation_input + + +PROBABILITIES = np.array([ + 0.0006917643261373052, + 0.015724004731018214, + 0.1261730210273901, + 0.3574112099154543, + 0.3574112099154544, + 0.1261730210273902, + 0.01572400473101823, + 0.0006917643261373052, +]) + + +@pytest.fixture(scope="module") +def sealed_standard_results(): + return { + basis: QSEncodeInsight(basis=basis).analyze(PROBABILITIES) + for basis in ("walsh", "fourier") + } + + +def test_sealed_walsh_refuses_compression_and_prepares_dense_baseline( + sealed_standard_results, +): + result = sealed_standard_results["walsh"] + baseline = result.selection.baseline_resource + + assert result.error_budget.k_star == 4 + assert result.selection.decision is SelectionDecision.DO_NOT_COMPRESS + assert baseline.compiled_two_qubit_gates == 21.0 + assert baseline.compiled_depth == 37.0 + assert result.evidence_scope.status is EvidenceScopeStatus.VALIDATED_DEFAULT + + artifact = QSEncodeInsight(basis="walsh").prepare( + PROBABILITIES, result=result + ) + assert artifact.selected_candidate_id == "dense_full__amplitude_encode" + assert artifact.k is None + + +def test_sealed_fourier_winner_resources_and_attribution(sealed_standard_results): + result = sealed_standard_results["fourier"] + baseline = result.selection.baseline_resource + dense = next( + candidate + for candidate in result.candidates + if candidate.candidate_id == "compressed__k4__amplitude_encode" + ) + winner = next( + candidate + for candidate in result.candidates + if candidate.candidate_id == "compressed__k4__sparse_isometry" + ) + + assert result.error_budget.k_star == 4 + assert result.selection.decision is SelectionDecision.COMPRESS + assert result.selection.selected_candidate_id == winner.candidate_id + assert baseline.compiled_two_qubit_gates == 114.0 + assert abs(baseline.compiled_depth - 205.0) <= 1.0 + assert dense.resource_audit.compiled_two_qubit_gates == 73.0 + assert abs(dense.resource_audit.compiled_depth - 123.0) <= 1.0 + assert winner.resource_audit.compiled_two_qubit_gates == 37.0 + # PyQPanda3 transpilation is technically non-deterministic. The frozen + # five-repeat median is 62, while the same validated environment can emit + # 61 without changing the selected method or the resource direction. + assert abs(winner.resource_audit.compiled_depth - 62.0) <= 1.0 + attribution = result.attribution + assert ( + attribution.total_two_qubit_difference, + attribution.truncation_two_qubit_difference, + attribution.preparation_two_qubit_difference, + ) == (77.0, 41.0, 36.0) + assert attribution.total_depth_difference == ( + baseline.compiled_depth - winner.resource_audit.compiled_depth + ) + assert attribution.truncation_depth_difference == ( + baseline.compiled_depth - dense.resource_audit.compiled_depth + ) + assert attribution.preparation_depth_difference == ( + dense.resource_audit.compiled_depth - winner.resource_audit.compiled_depth + ) + assert attribution.two_qubit_identity_error == 0.0 + assert attribution.depth_identity_error == 0.0 + + +def test_sealed_ds_preparation_register_semantics(): + compatible_state = np.array( + [1 / np.sqrt(3), 0, 0, 1j / np.sqrt(3), 0, -1 / np.sqrt(3), 0, 0] + ) + prepared = adapt_preparation_input(compatible_state) + assessment = assess_capability( + PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + prepared, + available_qubits=6, + ) + + assert assessment.report.required_qubits == 6 + assert assessment.report.compatible + assert assessment.build.output_qubits == (3, 4, 5) + assert assessment.build.ancillas == (0, 1, 2) + assert assessment.report.logical_fidelity >= 1.0 - 1e-10 + + +@pytest.mark.parametrize("basis", ["walsh", "fourier"]) +def test_sealed_audit_is_exactly_five_of_five(basis): + result = QSEncodeInsight(basis=basis, verification="audit").analyze( + PROBABILITIES + ) + + assert result.semantic_verification.status == "certified_pass" + assert result.semantic_verification.recommendation_valid is True + assert result.semantic_verification.technical_repetitions == 5 + assert len(result.semantic_verification.attempts) == 5 + assert {attempt.attempt_index for attempt in result.semantic_verification.attempts} == set(range(5)) + assert result.semantic_verification.minimum_fidelity >= 1.0 - 1e-10 + + +def test_sealed_binding_and_evidence_scope(sealed_standard_results): + result = sealed_standard_results["walsh"] + with pytest.raises(ResultBindingError) as error: + QSEncodeInsight(basis="walsh").prepare( + np.roll(PROBABILITIES, 1), result=result + ) + assert error.value.code == "input_mismatch" + + outside = QSEncodeInsight( + basis="walsh", fidelity_target=0.98 + ).analyze(PROBABILITIES) + assert outside.evidence_scope.status is EvidenceScopeStatus.OUTSIDE_VALIDATED_SCOPE diff --git a/test/QAlgBase/test_qseencode_selector.py b/test/QAlgBase/test_qseencode_selector.py new file mode 100644 index 00000000..d59d79b2 --- /dev/null +++ b/test/QAlgBase/test_qseencode_selector.py @@ -0,0 +1,114 @@ +import pytest + +from pyqpanda_alg.QSEncode import ( + CandidateResult, + PreparationMethod, + ResourceAudit, + SelectionDecision, +) +from pyqpanda_alg.QSEncode._selection import ( + select_best_eligible, + select_resource_candidate, +) +from pyqpanda_alg.QSEncode.exceptions import BaselineConstructionError + + +def _audit(twoq, depth, required=2, ancillas=()): + return ResourceAudit( + compiled_two_qubit_gates=float(twoq), + compiled_depth=float(depth), + compiled_total_gates=float(twoq + depth), + required_qubits=required, + allocated_qubits=4, + repetitions=5, + ancillas=tuple(ancillas), + compiled_cnot_gates=float(twoq), + valid=True, + status="valid", + successful_attempts=5, + ) + + +def _candidate(name, method, k, twoq, depth, required=2, ancillas=(), fidelity=0.99, eligible=True): + return CandidateResult( + candidate_id=name, + method=method, + k=k, + status="eligible" if eligible else "fidelity_ineligible", + retained_fidelity=fidelity, + eligible=eligible, + eligibility_reason="eligible" if eligible else "fidelity_below_target", + resource_audit=_audit(twoq, depth, required, ancillas), + ) + + +@pytest.mark.parametrize( + ("left", "right", "winner"), + [ + (_candidate("a", PreparationMethod.DS_QUANTUM_STATE_PREPARATION, 9, 1, 999, 8, range(4)), _candidate("b", PreparationMethod.AMPLITUDE_ENCODE, 1, 2, 1), "a"), + (_candidate("a", PreparationMethod.DS_QUANTUM_STATE_PREPARATION, 9, 2, 3, 8, range(4)), _candidate("b", PreparationMethod.AMPLITUDE_ENCODE, 1, 2, 4), "a"), + (_candidate("a", PreparationMethod.DS_QUANTUM_STATE_PREPARATION, 9, 2, 3, 3, range(2)), _candidate("b", PreparationMethod.AMPLITUDE_ENCODE, 1, 2, 3, 4), "a"), + (_candidate("a", PreparationMethod.DS_QUANTUM_STATE_PREPARATION, 9, 2, 3, 4, (0,)), _candidate("b", PreparationMethod.AMPLITUDE_ENCODE, 1, 2, 3, 4, (0, 1)), "a"), + (_candidate("a", PreparationMethod.DS_QUANTUM_STATE_PREPARATION, 2, 2, 3, 4, (0,)), _candidate("b", PreparationMethod.AMPLITUDE_ENCODE, 3, 2, 3, 4, (0,)), "a"), + (_candidate("a", PreparationMethod.AMPLITUDE_ENCODE, 2, 2, 3, 4, (0,)), _candidate("b", PreparationMethod.SPARSE_ISOMETRY, 2, 2, 3, 4, (0,)), "a"), + ], + ids=["twoq", "depth", "required", "ancilla", "k", "method_order"], +) +def test_exact_six_key_lexicographic_selector(left, right, winner): + assert select_best_eligible((right, left)).candidate_id == winner + + +def test_fidelity_is_eligibility_not_resource_tiebreak(): + lower_resource = _candidate("low", PreparationMethod.AMPLITUDE_ENCODE, 2, 1, 1, fidelity=0.9901) + higher_fidelity = _candidate("high-f", PreparationMethod.SPARSE_ISOMETRY, 2, 2, 2, fidelity=1.0) + assert select_best_eligible((higher_fidelity, lower_resource)).candidate_id == "low" + + +def test_ineligible_candidate_never_participates_even_with_zero_resources(): + ineligible = _candidate("bad", PreparationMethod.AMPLITUDE_ENCODE, 1, 0, 0, eligible=False) + eligible = _candidate("good", PreparationMethod.SPARSE_ISOMETRY, 2, 5, 5) + assert select_best_eligible((ineligible, eligible)).candidate_id == "good" + + +@pytest.mark.parametrize( + ("twoq", "depth", "expected"), + [ + (10, 20, SelectionDecision.DO_NOT_COMPRESS), + (9, 99, SelectionDecision.COMPRESS), + (99, 19, SelectionDecision.COMPRESS), + ], +) +def test_dnc_requires_no_improvement_in_both_primary_resources(twoq, depth, expected): + baseline = _candidate("dense_full", PreparationMethod.AMPLITUDE_ENCODE, 8, 10, 20) + compressed = _candidate("compressed", PreparationMethod.SPARSE_ISOMETRY, 3, twoq, depth) + selection = select_resource_candidate(baseline, (compressed,)) + assert selection.decision is expected + if expected is SelectionDecision.COMPRESS: + assert selection.selected_candidate_id == "compressed" + else: + assert selection.selected_candidate_id is None + assert selection.best_compressed_candidate_id == "compressed" + + +def test_no_eligible_candidate_is_normal_do_not_compress_result(): + baseline = _candidate("dense_full", PreparationMethod.AMPLITUDE_ENCODE, 8, 10, 20) + bad = _candidate("bad", PreparationMethod.SPARSE_ISOMETRY, 3, 1, 1, eligible=False) + result = select_resource_candidate(baseline, (bad,)) + assert result.decision is SelectionDecision.DO_NOT_COMPRESS + assert result.reason_code == "no_eligible_compressed_candidate" + + +def test_invalid_dense_full_baseline_is_not_disguised_as_dnc(): + baseline = _candidate("dense_full", PreparationMethod.AMPLITUDE_ENCODE, 8, 10, 20) + baseline = CandidateResult( + candidate_id=baseline.candidate_id, + method=baseline.method, + k=baseline.k, + status="baseline_failure", + resource_audit=ResourceAudit( + None, None, None, 3, 6, 5, valid=False, status="compile_failure" + ), + ) + with pytest.raises(BaselineConstructionError) as exc_info: + select_resource_candidate(baseline, ()) + assert exc_info.value.code == "dense_full_resource_invalid" diff --git a/test/QAlgBase/test_qseencode_verification.py b/test/QAlgBase/test_qseencode_verification.py new file mode 100644 index 00000000..6b9c9f8d --- /dev/null +++ b/test/QAlgBase/test_qseencode_verification.py @@ -0,0 +1,235 @@ +import numpy as np +import pytest +from pyqpanda3.core import H, QProg + +from pyqpanda_alg.QSEncode import PreparationMethod, VerificationLevel +from pyqpanda_alg.QSEncode._compiler import CompilationAttempt +from pyqpanda_alg.QSEncode._verification import ( + SEMANTIC_TOLERANCE, + audit_compiled_attempts, + certify_compiled_attempt, + verify_resource_selection, +) + + +def _program_with_h(qubit_count=2): + program = QProg(qubit_count) + program << H(qubit_count - 1) + return program + + +def _attempt(index, program, *, success=True): + return CompilationAttempt( + attempt_index=index, + success=success, + status="success" if success else "compile_failure", + compiled_program=program if success else None, + compiled_originir="originir" if success else None, + originir_sha256=f"hash-{index}" if success else None, + compiled_depth=1 if success else None, + compiled_total_gates=1 if success else None, + compiled_one_qubit_gates=1 if success else None, + compiled_two_qubit_gates=0 if success else None, + compiled_cnot_gates=0 if success else None, + required_qubits=1, + allocated_qubits=2, + ancilla_count=0, + compiler_profile_fingerprint="profile", + compiler_profile={}, + ) + + +def test_compiled_semantic_certification_is_phase_sensitive_and_global_phase_safe(): + logical = _program_with_h() + compiled = _program_with_h() + + evidence = certify_compiled_attempt( + logical, + compiled, + output_qubits=(1,), + method=PreparationMethod.AMPLITUDE_ENCODE, + attempt_index=0, + ) + + assert evidence.status == "certified_pass" + assert evidence.fidelity >= 1.0 - SEMANTIC_TOLERANCE + assert evidence.mapping_method == "output_register_constrained_permutation" + + +def test_standard_mode_does_not_call_compiled_semantic_verifier(monkeypatch): + import pyqpanda_alg.QSEncode._verification as module + + monkeypatch.setattr( + module, + "certify_compiled_attempt", + lambda *args, **kwargs: pytest.fail("standard must not run semantic sweep"), + ) + result = module.standard_verification() + + assert result.level is VerificationLevel.STANDARD + assert result.status == "not_run_by_standard" + assert result.recommendation_valid is True + assert result.technical_repetitions == 0 + assert result.attempts == () + + +def test_audit_uses_exactly_existing_five_attempts_and_never_compiles(monkeypatch): + import pyqpanda_alg.QSEncode._verification as module + + calls = [] + monkeypatch.setattr( + module, + "certify_compiled_attempt", + lambda *args, attempt_index, **kwargs: calls.append(attempt_index) + or module.SemanticVerificationAttempt( + attempt_index=attempt_index, + status="certified_pass", + fidelity=1.0, + mapping_method="synthetic", + output_qubits=(1,), + mapping=(1,), + ancilla_treatment="partial_trace", + ), + ) + program = _program_with_h() + attempts = tuple(_attempt(index, program) for index in range(5)) + verification = module.audit_compiled_attempts( + logical_program=program, + attempts=attempts, + output_qubits=(1,), + method=PreparationMethod.AMPLITUDE_ENCODE, + selected_candidate_id="winner", + ) + + assert calls == [0, 1, 2, 3, 4] + assert verification.status == "certified_pass" + assert verification.recommendation_valid is True + assert verification.technical_repetitions == 5 + + +def test_any_audit_failure_invalidates_recommendation_without_reselection(monkeypatch): + import pyqpanda_alg.QSEncode._verification as module + + program = _program_with_h() + attempts = tuple(_attempt(index, program) for index in range(5)) + + def synthetic(*args, attempt_index, **kwargs): + return module.SemanticVerificationAttempt( + attempt_index=attempt_index, + status="certified_fail" if attempt_index == 2 else "certified_pass", + fidelity=0.5 if attempt_index == 2 else 1.0, + mapping_method="synthetic", + output_qubits=(1,), + mapping=(1,), + ancilla_treatment="partial_trace", + ) + + monkeypatch.setattr(module, "certify_compiled_attempt", synthetic) + verification = module.audit_compiled_attempts( + logical_program=program, + attempts=attempts, + output_qubits=(1,), + method=PreparationMethod.SPARSE_ISOMETRY, + selected_candidate_id="compressed__k4__sparse_isometry", + ) + + assert verification.status == "certified_fail" + assert verification.recommendation_valid is False + assert verification.selected_candidate_id == "compressed__k4__sparse_isometry" + assert len(verification.attempts) == 5 + + +def test_compile_failure_is_retained_as_uncertified_attempt(): + program = _program_with_h() + attempts = tuple( + _attempt(index, program, success=index != 3) for index in range(5) + ) + verification = audit_compiled_attempts( + logical_program=program, + attempts=attempts, + output_qubits=(1,), + method=PreparationMethod.AMPLITUDE_ENCODE, + selected_candidate_id="baseline", + ) + + assert verification.status == "uncertified" + assert verification.recommendation_valid is False + assert verification.attempts[3].status == "compile_unavailable" + + +def test_internal_invariant_error_propagates(monkeypatch): + import pyqpanda_alg.QSEncode._verification as module + from pyqpanda_alg.QSEncode import InternalInvariantError + + program = _program_with_h() + attempts = tuple(_attempt(index, program) for index in range(5)) + + def fail(*args, **kwargs): + raise InternalInvariantError(code="synthetic_internal_failure") + + monkeypatch.setattr(module, "certify_compiled_attempt", fail) + with pytest.raises(InternalInvariantError) as error: + module.audit_compiled_attempts( + logical_program=program, + attempts=attempts, + output_qubits=(1,), + method=PreparationMethod.AMPLITUDE_ENCODE, + selected_candidate_id="baseline", + ) + assert error.value.code == "synthetic_internal_failure" + + +def test_ds_uses_ordered_output_subset_with_ancilla_trace_when_needed(): + logical = QProg(4) + logical << H(2) + compiled = QProg(4) + compiled << H(0) + + evidence = certify_compiled_attempt( + logical, + compiled, + output_qubits=(2, 3), + method=PreparationMethod.DS_QUANTUM_STATE_PREPARATION, + attempt_index=0, + ) + + assert evidence.status == "certified_pass" + assert evidence.mapping_method == "ordered_output_subset_with_ancilla_trace" + assert evidence.fidelity >= 1.0 - SEMANTIC_TOLERANCE + + +def test_frozen_small_cases_reuse_resource_attempts_for_audit(): + from pyqpanda_alg.QSEncode import SelectionDecision + from pyqpanda_alg.QSEncode._error_budget import find_k_star + from pyqpanda_alg.QSEncode._selection import run_resource_selection + from pyqpanda_alg.QSEncode._transforms import normalized_fourier, normalized_fwht + + probabilities = np.array([ + 0.0006917643261373052, 0.015724004731018214, + 0.1261730210273901, 0.3574112099154543, + 0.3574112099154544, 0.1261730210273902, + 0.01572400473101823, 0.0006917643261373052, + ]) + cases = ( + ("walsh", normalized_fwht(np.sqrt(probabilities)), SelectionDecision.DO_NOT_COMPRESS), + ("fourier", normalized_fourier(np.sqrt(probabilities)), SelectionDecision.COMPRESS), + ) + for basis, coefficients, expected_decision in cases: + budget = find_k_star(coefficients, 0.99) + run = run_resource_selection( + coefficients, basis=basis, error_budget=budget + ) + selection_before = run.selection + verification = verify_resource_selection( + coefficients, + basis=basis, + run=run, + level=VerificationLevel.AUDIT, + ) + + assert run.selection is selection_before + assert run.selection.decision is expected_decision + assert verification.status == "certified_pass" + assert verification.recommendation_valid is True + assert len(verification.attempts) == 5 + assert verification.minimum_fidelity >= 1.0 - SEMANTIC_TOLERANCE