Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions contest2/OriginQCup_DU_Fanta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# 量子风险分析:基于振幅估计的 VaR 与预期损失

**参赛队伍**:DU_Fanta | **任务类型**:创新应用 | **关联 Issue**:#13

---

## 一、这个应用做什么

在险价值(Value at Risk, VaR)和预期损失(Expected Shortfall, ES)是巴塞尔协议下最核心的两个风险指标。银行通常用蒙特卡洛模拟计算它们:要把误差降到 `eps`,需要 `O(1/eps^2)` 次采样。

本应用把 `pyqpanda_alg` 中已有的两个组件组合成一条完整的风险计量流水线:

| 组件 | 作用 |
| --- | --- |
| `QCmp.int_comparator` | 构造可逆判定电路 `L >= t` |
| `QAE.IQAE` | 估计该判定成立的概率,即尾部概率 `P(L >= t)` |

迭代振幅估计只需 `O(1/eps)` 次 oracle 查询,相对经典蒙特卡洛在**该子过程**上具有平方级优势。

### 关键设计:整条流水线只依赖尾部概率

本应用的核心观察是:**VaR 与 ES 都可以只用尾部概率表示**,因此一个量子原语就足够,不需要为 ES 单独设计带线性幅度旋转的电路。

对整数取值的损失 `L`:

```
E[(L - t)^+] = sum_{k > t} P(L >= k)

ES_alpha = VaR_alpha + E[(L - VaR_alpha)^+] / P(L >= VaR_alpha)
```

于是:

- **VaR** :对 `t` 做二分搜索,只需 `O(log N)` 次振幅估计(而非线性扫描的 `O(N)` 次);
- **ES** :VaR 之上若干个尾部概率求和即可。

两个恒等式均在 `Test_quantum_risk.py` 中对照精确值验证。

---

## 二、文件说明

| 文件 | 内容 |
| --- | --- |
| `quantum_risk.py` | 核心模块:`LossDistribution`、`QuantumRiskAnalyzer` |
| `example_credit_risk.py` | 信贷组合的完整演示 |
| `Test_quantum_risk.py` | 26 个单元测试,全部对照经典精确值 |

---

## 三、快速开始

```bash
pip install pyqpanda3 pyqpanda_alg numpy
cd contest2/OriginQCup_DU_Fanta
python example_credit_risk.py # 运行演示
python -m pytest Test_quantum_risk.py # 运行测试
```

最小示例:

```python
from quantum_risk import LossDistribution, QuantumRiskAnalyzer, recommended_epsilon

# 5 个债务人,各自的违约概率与整数敞口
dist = LossDistribution.from_credit_portfolio(
default_probabilities=[0.08, 0.15, 0.04, 0.22, 0.10],
exposures=[2, 1, 3, 1, 2],
unit=1e5,
)

confidence = 0.99
analyzer = QuantumRiskAnalyzer(dist, epsilon=recommended_epsilon(confidence))
summary = analyzer.report(confidence)

print(summary['var_monetary']) # 500000.0
print(summary['expected_shortfall_monetary']) # 约 530000
```

---

## 四、实测结果

演示所用组合:5 个债务人,总敞口 90 万元,期望损失 8.5 万元;损失分布占 4 个量子比特(16 个取值),加上比较器共 **8 个量子比特**。

### 尾部概率(量子估计 vs 精确值,eps = 0.01)

| 阈值 | 精确值 | 量子估计 | 绝对误差 |
| ---: | ---: | ---: | ---: |
| 1 | 0.47299 | 0.48212 | 0.00913 |
| 2 | 0.23135 | 0.23830 | 0.00695 |
| 3 | 0.10074 | 0.10271 | 0.00198 |
| 4 | 0.03092 | 0.02816 | 0.00276 |
| 5 | 0.01056 | 0.01015 | 0.00041 |
| 6 | 0.00278 | 0.00247 | 0.00032 |

### 风险指标

| 置信度 | eps | VaR(量子) | VaR(精确) | ES(量子) | ES(精确) | ES 误差 | 振幅估计次数 |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 0.90 | 0.0100 | 3 | 3 | 3.3785 | 3.4459 | 0.0673 | 18 |
| 0.95 | 0.0100 | 3 | 3 | 3.4660 | 3.4459 | 0.0201 | 18 |
| 0.99 | 0.0020 | 5 | 5 | 5.2986 | 5.3256 | 0.0271 | 16 |

三个置信度下 VaR 均与精确值完全一致。

### 查询次数对比

| 精度 eps | 经典蒙特卡洛采样数 |
| ---: | ---: |
| 0.05 | 738 |
| 0.01 | 18,445 |
| 0.005 | 73,778 |
| 0.001 | 1,844,440 |

经典方法为 `O(1/eps^2)`,迭代振幅估计为 `O(1/eps)`。

---

## 五、一个必须说明的精度条件

VaR 的二分搜索需要判断 `P(L >= t+1) <= 1 - alpha`。**如果估计精度 `eps` 与 `1 - alpha` 相当,这个比较就会被估计噪声主导**,返回的 VaR 可能相差一档。

开发过程中在 `alpha = 0.99`、`eps = 0.01` 下确实观察到了该现象:真实 `P(L >= 5) = 0.01056`,与判定阈值 `0.01` 的差距小于 `eps`,导致 VaR 被低估为 4。

因此模块提供 `recommended_epsilon(alpha) = 0.2 * (1 - alpha)`,并在 `eps` 过粗时发出 `RuntimeWarning`。上表中 `alpha = 0.99` 使用 `eps = 0.002` 后结果正确。

---

## 六、本应用不主张的内容

- **不主张端到端的量子加速。** 平方级优势存在于尾部概率这一子过程的查询复杂度上。整条流水线还包含经典的分布构造与二分搜索;在模拟器上,经典计算本身更快。
- **不主张已可用于生产。** 演示规模为 4 个损失比特(16 个取值)。真实组合需要更多比特、相关性违约模型,以及在含噪声硬件上的误差缓解。
- **未在真实量子硬件上运行。** `QAE.IQAE` 目前仅支持 CPU 模拟器后端。

---

## English summary

**Quantum risk analysis: Value at Risk and Expected Shortfall via amplitude estimation.**

This application composes two existing `pyqpanda_alg` components — `QCmp.int_comparator` (the reversible predicate `L >= t`) and `QAE.IQAE` (amplitude estimation) — into a complete risk-measurement workflow.

The key design point is that **both VaR and Expected Shortfall reduce to tail probabilities**, so one quantum primitive suffices. For integer-valued losses:

```
E[(L - t)^+] = sum_{k > t} P(L >= k)
ES_alpha = VaR_alpha + E[(L - VaR_alpha)^+] / P(L >= VaR_alpha)
```

VaR therefore comes from a bisection over tail probabilities in `O(log N)` amplitude estimations instead of an `O(N)` scan, and ES from a short sum of them. No separate estimation primitive with linear-amplitude rotations is needed.

**Results** on a 5-obligor credit portfolio (8 qubits: 4 loss + 4 comparator): VaR matches the exact value at all three confidence levels, Expected Shortfall to within 0.07 loss levels, using 16–18 amplitude estimations.

**Accuracy condition.** The VaR bisection compares tail probabilities against `1 - alpha`, so `epsilon` must be small relative to that gap. This was observed concretely at `alpha = 0.99, epsilon = 0.01`, where the true `P(L >= 5) = 0.01056` sits within `epsilon` of the `0.01` decision threshold and VaR was under-estimated. The module exposes `recommended_epsilon(alpha) = 0.2 * (1 - alpha)` and warns when `epsilon` is too coarse.

**Not claimed:** end-to-end quantum speedup (the quadratic advantage is in the query complexity of the tail-probability subroutine only), production readiness, or any hardware run — `QAE.IQAE` currently supports the CPU simulator backend only.

**Tests:** 26 unit tests, every quantum estimate checked against the exact classical value.

## References

1. S. Woerner, D. J. Egger. *Quantum risk analysis.* npj Quantum Information **5**, 15 (2019).
2. D. Grinko, J. Gacon, C. Zoufal, S. Woerner. *Iterative quantum amplitude estimation.* npj Quantum Information **7**, 52 (2021).
205 changes: 205 additions & 0 deletions contest2/OriginQCup_DU_Fanta/Test_quantum_risk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for the quantum risk analysis application.

Every quantum estimate is checked against the exact value computed classically
from the same distribution, so the tests verify the workflow rather than merely
exercising it.
"""

import itertools

import numpy as np
import pytest

from quantum_risk import (
LossDistribution,
QuantumRiskAnalyzer,
classical_monte_carlo_samples,
recommended_epsilon,
)

SIMPLE_PROBABILITIES = [0.30, 0.25, 0.18, 0.12, 0.08, 0.04, 0.02, 0.01]
PORTFOLIO_DEFAULTS = [0.10, 0.20, 0.05, 0.30]
PORTFOLIO_EXPOSURES = [1, 2, 1, 3]


class Test_LossDistribution:

def test_normalises_and_pads_to_power_of_two(self):
dist = LossDistribution([1.0, 1.0, 1.0])
assert dist.probabilities.size == 4, "概率向量应补齐到 2 的幂"
assert dist.num_qubits == 2, "3 个取值需要 2 个量子比特"
assert abs(dist.probabilities.sum() - 1.0) < 1e-12, "概率应归一化"
assert dist.probabilities[3] == 0.0, "补齐位置的概率应为 0"

def test_tail_probability_matches_direct_sum(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
probabilities = np.asarray(SIMPLE_PROBABILITIES) / sum(SIMPLE_PROBABILITIES)
for threshold in range(len(SIMPLE_PROBABILITIES) + 1):
expected = probabilities[threshold:].sum()
assert abs(dist.tail_probability(threshold) - expected) < 1e-12, \
f"阈值 {threshold} 的尾部概率不正确"

def test_expected_shortfall_matches_conditional_mean(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
for confidence in (0.5, 0.8, 0.9, 0.95):
var = dist.value_at_risk(confidence)
mask = dist.levels >= var
expected = (dist.levels[mask] * dist.probabilities[mask]).sum() / \
dist.tail_probability(var)
assert abs(dist.expected_shortfall(confidence) - expected) < 1e-12, \
f"置信度 {confidence} 的期望损失不等于条件均值"

def test_credit_portfolio_matches_brute_force(self):
"""卷积得到的组合损失分布应与穷举所有违约组合一致。"""
dist = LossDistribution.from_credit_portfolio(PORTFOLIO_DEFAULTS, PORTFOLIO_EXPOSURES)
brute = np.zeros(sum(PORTFOLIO_EXPOSURES) + 1)
for indicators in itertools.product([0, 1], repeat=len(PORTFOLIO_DEFAULTS)):
probability, loss = 1.0, 0
for indicator, default, exposure in zip(indicators, PORTFOLIO_DEFAULTS,
PORTFOLIO_EXPOSURES):
probability *= default if indicator else (1.0 - default)
loss += exposure * indicator
brute[loss] += probability
padded = np.zeros(dist.probabilities.size)
padded[:brute.size] = brute
assert np.max(np.abs(dist.probabilities - padded)) < 1e-12, "组合损失分布与穷举结果不一致"

def test_state_preparation_reproduces_the_distribution(self):
"""态制备电路产生的测量概率应等于目标分布。"""
from pyqpanda3.core import CPUQVM, QProg

dist = LossDistribution(SIMPLE_PROBABILITIES)
qubits = list(range(dist.num_qubits))
prog = QProg()
prog << dist.state_preparation(qubits)
machine = CPUQVM()
machine.run(prog, 1)
amplitudes = np.asarray(machine.result().get_state_vector())
measured = np.abs(amplitudes) ** 2
assert np.max(np.abs(measured - dist.probabilities)) < 1e-9, "态制备结果与目标分布不符"

@pytest.mark.parametrize("probabilities", [[], [-0.1, 0.5], [0.0, 0.0]])
def test_rejects_invalid_probabilities(self, probabilities):
with pytest.raises(ValueError):
LossDistribution(probabilities)

def test_rejects_mismatched_portfolio_lengths(self):
with pytest.raises(ValueError):
LossDistribution.from_credit_portfolio([0.1, 0.2], [1])

@pytest.mark.parametrize("confidence", [0.0, 1.0, -0.5, 1.5])
def test_rejects_invalid_confidence(self, confidence):
dist = LossDistribution(SIMPLE_PROBABILITIES)
with pytest.raises(ValueError):
dist.value_at_risk(confidence)


class Test_QuantumRiskAnalyzer:

def test_tail_probability_matches_exact_value(self):
"""振幅估计得到的尾部概率应落在给定精度内。"""
dist = LossDistribution(SIMPLE_PROBABILITIES)
epsilon = 0.01
analyzer = QuantumRiskAnalyzer(dist, epsilon=epsilon)
for threshold in range(1, dist.probabilities.size):
exact = dist.tail_probability(threshold)
estimate = analyzer.tail_probability(threshold)
assert abs(estimate - exact) < 10 * epsilon, \
f"阈值 {threshold}: 估计值 {estimate:.5f} 与精确值 {exact:.5f} 偏差过大"

def test_tail_probability_boundaries_need_no_estimation(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
analyzer = QuantumRiskAnalyzer(dist, epsilon=0.05)
assert analyzer.tail_probability(0) == 1.0, "阈值为 0 时尾部概率必为 1"
assert analyzer.tail_probability(dist.probabilities.size) == 0.0, \
"阈值超出取值范围时尾部概率必为 0"
assert analyzer.estimate_calls == 0, "边界情形不应调用振幅估计"

def test_value_at_risk_matches_exact_value(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
for confidence in (0.80, 0.90):
analyzer = QuantumRiskAnalyzer(dist, epsilon=recommended_epsilon(confidence))
assert analyzer.value_at_risk(confidence) == dist.value_at_risk(confidence), \
f"置信度 {confidence} 下的 VaR 与精确值不一致"

def test_value_at_risk_uses_logarithmic_number_of_estimations(self):
"""二分搜索的振幅估计次数应为 O(log2(取值数)),而非线性扫描。"""
dist = LossDistribution(SIMPLE_PROBABILITIES)
analyzer = QuantumRiskAnalyzer(dist, epsilon=0.02)
analyzer.estimate_calls = 0
analyzer.value_at_risk(0.80)
levels = dist.probabilities.size
assert analyzer.estimate_calls <= int(np.ceil(np.log2(levels))) + 1, \
f"{levels} 个取值上的二分搜索调用了 {analyzer.estimate_calls} 次振幅估计"

def test_expected_shortfall_matches_exact_value(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
confidence = 0.80
analyzer = QuantumRiskAnalyzer(dist, epsilon=recommended_epsilon(confidence))
exact = dist.expected_shortfall(confidence)
estimate = analyzer.expected_shortfall(confidence)
assert abs(estimate - exact) < 0.5, \
f"期望损失估计 {estimate:.4f} 与精确值 {exact:.4f} 偏差过大"

def test_report_contains_quantum_and_exact_values(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
confidence = 0.80
analyzer = QuantumRiskAnalyzer(dist, epsilon=recommended_epsilon(confidence))
summary = analyzer.report(confidence)
for key in ('var_quantum', 'var_exact', 'expected_shortfall_quantum',
'expected_shortfall_exact', 'amplitude_estimations', 'num_qubits'):
assert key in summary, f"报告缺少字段 {key}"
assert summary['num_qubits'] == 2 * dist.num_qubits, "寄存器宽度应为损失寄存器的两倍"
assert summary['amplitude_estimations'] > 0, "报告应记录振幅估计的调用次数"

def test_warns_when_epsilon_cannot_resolve_the_confidence_level(self):
"""精度不足以分辨 1 - alpha 时应给出警告。"""
dist = LossDistribution(SIMPLE_PROBABILITIES)
analyzer = QuantumRiskAnalyzer(dist, epsilon=0.05)
with pytest.warns(RuntimeWarning):
analyzer.value_at_risk(0.99)

def test_rejects_invalid_arguments(self):
dist = LossDistribution(SIMPLE_PROBABILITIES)
with pytest.raises(TypeError):
QuantumRiskAnalyzer(SIMPLE_PROBABILITIES)
with pytest.raises(ValueError):
QuantumRiskAnalyzer(dist, epsilon=0.0)
with pytest.raises(ValueError):
QuantumRiskAnalyzer(dist, epsilon=1.5)


class Test_query_complexity:

def test_recommended_epsilon_scales_with_the_tail(self):
assert recommended_epsilon(0.90) > recommended_epsilon(0.99), \
"置信度越高,所需精度应越小"
assert abs(recommended_epsilon(0.99) - 0.2 * 0.01) < 1e-12

def test_classical_sample_count_grows_quadratically(self):
coarse = classical_monte_carlo_samples(0.01)
fine = classical_monte_carlo_samples(0.005)
ratio = fine / coarse
assert 3.5 < ratio < 4.5, \
f"精度提高一倍时经典采样量应约变为 4 倍,实际比值为 {ratio:.2f}"

@pytest.mark.parametrize("epsilon", [0.0, 1.0, -0.1])
def test_classical_sample_count_rejects_invalid_accuracy(self, epsilon):
with pytest.raises(ValueError):
classical_monte_carlo_samples(epsilon)


if __name__ == "__main__":
pytest.main([__file__, "-v"])
Loading