File size: 11,404 Bytes
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4251dc
 
 
68c9302
 
 
 
 
 
 
a4251dc
 
 
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f611d8
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f611d8
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f611d8
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f611d8
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f611d8
a4251dc
 
 
 
68c9302
 
a4251dc
 
68c9302
 
a4251dc
 
68c9302
 
a4251dc
 
68c9302
 
 
a4251dc
68c9302
1f611d8
68c9302
 
a4251dc
68c9302
 
 
 
1f611d8
a4251dc
68c9302
 
 
 
a4251dc
68c9302
 
 
1f611d8
a4251dc
68c9302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4251dc
 
68c9302
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# -*- coding: utf-8 -*-
"""
Comprehensive test suite for the Qiskit-backed Quantum Circuit Optimization Environment.

Covers:
  1. Bell State   — H → CNOT → fidelity ≈ 1
  2. GHZ State    — H → CNOT(0,1) → CNOT(1,2) → fidelity ≈ 1
  3. SWAP         — verify qubit swap physically changes statevector
  4. REMOVE       — add gate → remove gate → state reverts
  5. Budget task  — smooth overflow penalty
  6. Noise impact — deeper circuits penalised
  7. STOP         — correct terminal reward threshold
  8. Redundancy   — repeated gates penalised
"""

import sys
import numpy as np

from server.my_env_environment import (
    QuantumCircuitEnvironment,
    compute_statevector,
    build_qiskit_circuit,
    MAX_QUBITS,
    MAX_DEPTH,
)
from models import ActionType, GateType, QuantumAction


PASS = "\033[92mPASS\033[0m"
FAIL = "\033[91mFAIL\033[0m"


def section(title: str) -> None:
    print(f"\n{'='*55}")
    print(f"  {title}")
    print(f"{'='*55}")


def check(cond: bool, msg: str) -> None:
    tag = PASS if cond else FAIL
    print(f"  [{tag}] {msg}")
    if not cond:
        raise AssertionError(msg)


# ---------------------------------------------------------------------------
# STEP 10 — Test 1: Bell State — H → CNOT → fidelity ≈ 1
# ---------------------------------------------------------------------------
def test_bell_state():
    section("TEST 1: Bell State — H → CNOT → fidelity ≈ 1")
    env = QuantumCircuitEnvironment(seed=42)
    obs = env.reset()
    print(f"  Initial fidelity: {obs.fidelity:.6f}, score: {obs.score:.6f}")

    obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0]))
    print(f"  After H(0):       fid={obs.fidelity:.6f} reward={obs.reward:+.6f}")

    obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1]))
    print(f"  After CNOT(0,1):  fid={obs.fidelity:.6f} reward={obs.reward:+.6f}")

    obs = env.step(QuantumAction(action_type=ActionType.STOP))
    check(obs.fidelity > 0.99, f"Bell fidelity {obs.fidelity:.6f} should be ≈ 1.0")
    check(obs.score > 0.5,     f"Bell aggregate score {obs.score:.6f} should be > 0.5")


# ---------------------------------------------------------------------------
# STEP 10 — Test 2: GHZ State — H → CNOT(0,1) → CNOT(1,2) → fidelity ≈ 1
# ---------------------------------------------------------------------------
def test_ghz_state():
    section("TEST 2: GHZ State — H → CNOT(0,1) → CNOT(1,2) → fidelity ≈ 1")
    env = QuantumCircuitEnvironment(seed=42)
    obs = env.reset()
    print(f"  Initial fidelity: {obs.fidelity:.6f}")

    obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0]))
    obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1]))
    obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[1, 2]))
    print(f"  After H+CNOT(0,1)+CNOT(1,2):  fid={obs.fidelity:.6f}")

    obs = env.step(QuantumAction(action_type=ActionType.STOP))
    check(obs.fidelity > 0.99, f"GHZ fidelity {obs.fidelity:.6f} should be ≈ 1.0")
    check(obs.score > 0.5,     f"GHZ aggregate score {obs.score:.6f} should be > 0.5")


# ---------------------------------------------------------------------------
# STEP 10 — Test 3: SWAP — verify qubit swap physically changes statevector
# ---------------------------------------------------------------------------
def test_swap_physics():
    section("TEST 3: SWAP — physical qubit routing verification")

    # Qiskit uses LITTLE-ENDIAN qubit ordering (qubit 0 = LSB).
    # X on qubit 0 sets the LSB → state occupies some basis index.
    # After SWAP(0,1) the occupied index must change (qubits exchanged).
    gates_x    = [{"gate": "X", "qubits": [0]}]
    sv_before  = compute_statevector(gates_x, num_qubits=2)
    gates_swap = [{"gate": "X", "qubits": [0]}, {"gate": "SWAP", "qubits": [0, 1]}]
    sv_after   = compute_statevector(gates_swap, num_qubits=2)

    idx_before = int(np.argmax(np.abs(sv_before)))
    idx_after  = int(np.argmax(np.abs(sv_after)))
    print(f"  Before SWAP: dominant index = {idx_before} (|amp|={abs(sv_before[idx_before]):.4f})")
    print(f"  After  SWAP: dominant index = {idx_after}  (|amp|={abs(sv_after[idx_after]):.4f})")

    check(
        abs(sv_before[idx_before]) > 0.99,
        f"Before SWAP must be pure basis state; |amp|={abs(sv_before[idx_before]):.4f}"
    )
    check(
        abs(sv_after[idx_after]) > 0.99,
        f"After SWAP must be pure basis state;  |amp|={abs(sv_after[idx_after]):.4f}"
    )
    check(
        idx_before != idx_after,
        f"SWAP must change the occupied basis index: before={idx_before}, after={idx_after}"
    )

    # Also verify it works correctly inside the RL environment
    env = QuantumCircuitEnvironment(seed=42)
    env.reset()
    env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.X, qubits=[0]))
    obs_before_swap = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[1]))
    fid_before = obs_before_swap.fidelity

    obs_after_swap = env.step(QuantumAction(action_type=ActionType.SWAP, qubits=[0, 1]))
    fid_after = obs_after_swap.fidelity
    print(f"  RL env — fidelity before SWAP: {fid_before:.6f}, after SWAP: {fid_after:.6f}")
    check(obs_after_swap.reward is not None, "SWAP action executed without error in RL environment")
    check(obs_after_swap.reward < 10.0,      "SWAP reward is finite and penalises SWAP count")


# ---------------------------------------------------------------------------
# STEP 10 — Test 4: REMOVE — add gate → remove gate → state reverts
# ---------------------------------------------------------------------------
def test_remove_reverts():
    section("TEST 4: REMOVE — state reverts after gate removal")
    env = QuantumCircuitEnvironment(seed=42)
    obs_empty = env.reset()
    fid_empty = obs_empty.fidelity
    sv_empty  = compute_statevector([], num_qubits=2)

    obs_h = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0]))
    fid_h = obs_h.fidelity
    print(f"  Fidelity (empty):          {fid_empty:.6f}")
    print(f"  Fidelity (after H(0)):     {fid_h:.6f}")
    check(abs(fid_h - fid_empty) > 1e-6, "H gate must change fidelity from empty state")

    obs_rm = env.step(QuantumAction(action_type=ActionType.REMOVE))
    fid_rm = obs_rm.fidelity
    sv_rm  = compute_statevector([], num_qubits=2)
    print(f"  Fidelity (after REMOVE):   {fid_rm:.6f}")
    check(
        abs(fid_rm - fid_empty) < 1e-6,
        f"After REMOVE fidelity {fid_rm:.6f} should revert to {fid_empty:.6f}"
    )

    # Verify statevectors match too (direct Qiskit call)
    check(
        np.allclose(sv_rm, sv_empty, atol=1e-10),
        "Statevector after REMOVE matches original empty statevector"
    )
    check(obs_rm.gate_count == 0, "Gate count should be 0 after REMOVE on 1-gate circuit")


# ---------------------------------------------------------------------------
# Original regression tests
# ---------------------------------------------------------------------------

def test_budget_penalty():
    section("TEST 5: Budget Task — smooth overflow penalty")
    env = QuantumCircuitEnvironment()
    env.reset()
    env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0]))
    env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1]))
    obs1 = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[1, 2]))
    s1 = obs1.score
    print(f"  Score at budget (3 gates): {s1:.4f}")

    obs2 = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[2]))
    s2 = obs2.score
    print(f"  Score at overflow (4 gates): {s2:.4f}")

    obs3 = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[2]))
    s3 = obs3.score
    print(f"  Score at overflow (5 gates): {s3:.4f}")
    check(s1 >= 0.0 and s2 >= 0.0 and s3 >= 0.0, "All budget scores are non-negative")


def test_stop_reward():
    section("TEST 6: STOP — correct terminal reward threshold")
    env = QuantumCircuitEnvironment()

    # High-score stop → reward should be +0.2
    env.reset()
    env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H,    qubits=[0]))
    env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.CNOT, qubits=[0, 1]))
    obs = env.step(QuantumAction(action_type=ActionType.STOP))
    print(f"  High-score STOP: score={obs.score:.4f} reward={obs.reward:.4f}")
    check(obs.done, "Episode must be done after STOP")

    # Low-score stop → reward should be -0.2
    env.reset()
    obs = env.step(QuantumAction(action_type=ActionType.STOP))
    print(f"  Low-score STOP:  score={obs.score:.4f} reward={obs.reward:.4f}")
    check(obs.done, "Episode must be done after low-score STOP")
    check(obs.reward <= 0.0, f"Low-score STOP reward {obs.reward:.4f} should be ≤ 0")


def test_redundancy_penalty():
    section("TEST 7: Redundancy penalty — same gate twice")
    env = QuantumCircuitEnvironment()
    env.reset()
    env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0]))
    obs = env.step(QuantumAction(action_type=ActionType.ADD, gate=GateType.H, qubits=[0]))
    print(f"  H(0) twice reward: {obs.reward:.4f} (expect negative contribution from redundancy)")
    # Reward contains multiple terms; just check it's computed without error
    check(obs.reward is not None, "Reward computed for redundant gate")


def test_module_constants():
    section("TEST 8: Module-level performance constants")
    from server.my_env_environment import MAX_QUBITS, MAX_STEPS, MAX_DEPTH
    check(MAX_QUBITS == 4,  f"MAX_QUBITS={MAX_QUBITS}, expected 4")
    check(MAX_STEPS  == 15, f"MAX_STEPS={MAX_STEPS}, expected 15")
    check(MAX_DEPTH  == 20, f"MAX_DEPTH={MAX_DEPTH}, expected 20")
    print(f"  MAX_QUBITS={MAX_QUBITS}  MAX_STEPS={MAX_STEPS}  MAX_DEPTH={MAX_DEPTH}")


def test_statevector_validation():
    section("TEST 9: Statevector dimension assertion")
    for n in (1, 2, 3, 4):
        sv = compute_statevector([], num_qubits=n)
        check(len(sv) == 2 ** n, f"Empty circuit, {n} qubits → len={len(sv)}, expected {2**n}")
        print(f"  {n} qubits: len={len(sv)} ✓")


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def run_all():
    tests = [
        test_bell_state,
        test_ghz_state,
        test_swap_physics,
        test_remove_reverts,
        test_budget_penalty,
        test_stop_reward,
        test_redundancy_penalty,
        test_module_constants,
        test_statevector_validation,
    ]
    passed = 0
    failed = 0
    for t in tests:
        try:
            t()
            passed += 1
        except AssertionError as e:
            print(f"  [FAIL] {e}")
            failed += 1
        except Exception as e:
            print(f"  [ERROR] {type(e).__name__}: {e}")
            failed += 1

    section("RESULTS")
    print(f"  Passed: {passed}/{len(tests)}")
    print(f"  Failed: {failed}/{len(tests)}")
    if failed:
        sys.exit(1)
    else:
        print("\n  ALL TESTS PASSED!")


if __name__ == "__main__":
    run_all()