Skip to content

Commit 239bd8d

Browse files
add problem 4 in suite
1 parent ea9e92f commit 239bd8d

4 files changed

Lines changed: 395 additions & 3 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Evaluation for Challenge Suite Problem 4.
3+
4+
The evaluator dynamically imports a solution module, consumes only the NumPy
5+
result dictionary returned by run_solution, and prints compact validation output.
6+
"""
7+
8+
import argparse
9+
import importlib
10+
import time
11+
12+
import numpy as np
13+
14+
DEFAULT_CONFIG = {
15+
"n_qubits": 12,
16+
"entangler_angle": 0.31,
17+
"true_p01": 0.034,
18+
"true_p10": 0.011,
19+
"initial_p01": 0.070,
20+
"initial_p10": 0.040,
21+
"max_steps": 120,
22+
"learning_rate": 0.04,
23+
}
24+
25+
PROBE_NAMES = ("zeros", "ones", "neel", "plus")
26+
27+
28+
def asymmetric_bitflip_kraus_numpy(p01, p10):
29+
return [
30+
np.array(
31+
[[np.sqrt(1.0 - p01), 0.0], [0.0, np.sqrt(1.0 - p10)]],
32+
dtype=np.complex128,
33+
),
34+
np.array([[0.0, np.sqrt(p10)], [0.0, 0.0]], dtype=np.complex128),
35+
np.array([[0.0, 0.0], [np.sqrt(p01), 0.0]], dtype=np.complex128),
36+
]
37+
38+
39+
def trace_preserving_error(p01, p10):
40+
kraus = asymmetric_bitflip_kraus_numpy(p01, p10)
41+
identity = np.eye(2, dtype=np.complex128)
42+
contraction = sum(k.conj().T @ k for k in kraus)
43+
return float(np.max(np.abs(contraction - identity)))
44+
45+
46+
def print_expectation_table(targets, fitted):
47+
observable_names = [f"Z{i}" for i in range(targets.shape[1] - 1)] + ["parity"]
48+
print("Expectation comparison:")
49+
for probe_index, probe_name in enumerate(PROBE_NAMES):
50+
for obs_index, obs_name in enumerate(observable_names):
51+
target = targets[probe_index, obs_index]
52+
value = fitted[probe_index, obs_index]
53+
print(
54+
f" {probe_name:15s} {obs_name:6s} "
55+
f"target={target: .8f} fitted={value: .8f} "
56+
f"abs_error={abs(target - value):.3e}"
57+
)
58+
59+
60+
def evaluate(solution_module, config):
61+
module = importlib.import_module(solution_module)
62+
63+
start = time.perf_counter()
64+
results = module.run_solution(config)
65+
elapsed = time.perf_counter() - start
66+
67+
targets = np.asarray(results["target_expectations"], dtype=float)
68+
fitted = np.asarray(results["fitted_expectations"], dtype=float)
69+
expectation_errors = np.abs(fitted - targets)
70+
final_p01 = float(results["final_p01"])
71+
final_p10 = float(results["final_p10"])
72+
absolute_error_p01 = abs(final_p01 - config["true_p01"])
73+
absolute_error_p10 = abs(final_p10 - config["true_p10"])
74+
tp_error = trace_preserving_error(final_p01, final_p10)
75+
76+
criteria = {
77+
"loss history length": len(results["loss_history"]) == config["max_steps"],
78+
"loss improves": float(results["final_loss"]) < float(results["initial_loss"]),
79+
"target shape": targets.shape == (len(PROBE_NAMES), config["n_qubits"] + 1),
80+
"fitted shape": fitted.shape == (len(PROBE_NAMES), config["n_qubits"] + 1),
81+
"p01 absolute error <= 1e-4": absolute_error_p01 <= 1e-4,
82+
"p10 absolute error <= 1e-4": absolute_error_p10 <= 1e-4,
83+
"trace preserving error <= 1e-8": tp_error <= 1e-8,
84+
}
85+
86+
print("Challenge 4 evaluation")
87+
print(f"Solution module: {solution_module}")
88+
print(f"End-to-end solution time: {elapsed:.2f}s")
89+
print(f"True p01, p10: {config['true_p01']:.8f}, {config['true_p10']:.8f}")
90+
print(
91+
"Initial p01, p10: "
92+
f"{float(results['initial_p01']):.8f}, {float(results['initial_p10']):.8f}"
93+
)
94+
print(f"Fitted p01, p10: {final_p01:.8f}, {final_p10:.8f}")
95+
print(f"Absolute p01 error: {absolute_error_p01:.8e}")
96+
print(f"Absolute p10 error: {absolute_error_p10:.8e}")
97+
print(f"Initial loss: {float(results['initial_loss']):.8e}")
98+
print(f"Final loss: {float(results['final_loss']):.8e}")
99+
print(f"Max expectation abs error: {float(np.max(expectation_errors)):.8e}")
100+
print(f"Mean expectation abs error: {float(np.mean(expectation_errors)):.8e}")
101+
print(f"Trace-preserving error: {tp_error:.8e}")
102+
print(f"Loss history length: {len(results['loss_history'])}")
103+
print(f"Returned NumPy keys: {sorted(results)}")
104+
print_expectation_table(targets, fitted)
105+
print("Passing criteria:")
106+
for name, passed in criteria.items():
107+
print(f" {name}: {'PASS' if passed else 'FAIL'}")
108+
print(f"Overall: {'PASS' if all(criteria.values()) else 'FAIL'}")
109+
110+
111+
def main():
112+
parser = argparse.ArgumentParser()
113+
parser.add_argument("--solution", default="solution_4")
114+
parser.add_argument("--max-steps", type=int, default=DEFAULT_CONFIG["max_steps"])
115+
args = parser.parse_args()
116+
117+
config = dict(DEFAULT_CONFIG)
118+
config["max_steps"] = args.max_steps
119+
evaluate(args.solution, config)
120+
121+
122+
if __name__ == "__main__":
123+
main()
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Problem 4: Trainable Kraus Noise Calibration From Multi-Circuit Data
2+
3+
## Goal
4+
5+
Fit the two parameters of a user-defined asymmetric bit-flip channel from synthetic multi-circuit observable data. The task includes generating the synthetic target observable table by running the same noisy probe circuits at the true channel probabilities, then fitting the channel probabilities from that generated table. The evaluator provides the true probabilities and probe configuration, but it does not provide precomputed target observables. The task is not to use a built-in noise model, but to express the channel as Kraus tensor algebra, insert it after fixed entangling operations, differentiate through the noisy circuits, and recover the true transition probabilities from expectation values.
6+
7+
## Fixed Problem Configuration
8+
9+
The evaluator defines and passes the following configuration dictionary into `run_solution(config)`.
10+
11+
```python
12+
{
13+
"n_qubits": 12,
14+
"entangler_angle": 0.31,
15+
"true_p01": 0.034,
16+
"true_p10": 0.011,
17+
"initial_p01": 0.070,
18+
"initial_p10": 0.040,
19+
"max_steps": 120,
20+
"learning_rate": 0.04,
21+
}
22+
```
23+
24+
The asymmetric bit-flip channel acts on one qubit as
25+
26+
$$K_0 = \sqrt{1-p_{01}} |0\rangle\langle 0| + \sqrt{1-p_{10}} |1\rangle\langle 1|,$$
27+
28+
$$K_1 = \sqrt{p_{10}} |0\rangle\langle 1|,\qquad K_2 = \sqrt{p_{01}} |1\rangle\langle 0|.$$
29+
30+
Here `p01` is the probability for `0 -> 1` and `p10` is the probability for `1 -> 0`. Inside `run_solution(config)`, first generate the synthetic target data from the true values `p01 = 0.034` and `p10 = 0.011`, using exactly the probe circuits and observables defined below. Then initialize the fitted probabilities from `initial_p01` and `initial_p10` and optimize them against that generated target table.
31+
32+
### Probe Circuits
33+
34+
Use four fixed 12-qubit product-state inputs and the same noisy probe circuit for every input. The four inputs are `|0>^12`, `|1>^12`, `|010101010101>`, and `|+>^12`.
35+
36+
The shared probe circuit is one even-bond brickwork entangler layer. On every bond `(0,1), (2,3), ..., (10,11)`, apply `RXX(0.31)` and then apply the asymmetric bit-flip channel independently to the two qubits in that bond.
37+
38+
All probes therefore have identical circuit structure and differ only in the initial state.
39+
40+
### Observables And Loss
41+
42+
For each probe circuit, use the noisy expectations of all single-site observables `Z_i` for `i = 0, ..., 11` and the full-chain parity `Z_0 Z_1 ... Z_11`. This gives a target table with shape `(4, 13)`.
43+
44+
Parameterize the fitted probabilities by two unconstrained scalars,
45+
46+
$$p_{01} = \operatorname{sigmoid}(r_{01}),\qquad p_{10} = \operatorname{sigmoid}(r_{10}).$$
47+
48+
Initialize these probabilities to `initial_p01 = 0.070` and `initial_p10 = 0.040`. Train the mean squared error between the fitted observable table and the synthetic target table for exactly 120 Adam updates at learning rate `0.04`. Do not use early stopping.
49+
50+
## Solution Interface
51+
52+
The solution file must be named `solution_4.py` and expose:
53+
54+
```python
55+
def run_solution(config):
56+
...
57+
return results
58+
```
59+
60+
The solution should not print progress. It should perform the core computation and return only the NumPy-format quantities that the evaluator checks.
61+
62+
Required result keys:
63+
64+
- `initial_p01`: scalar float.
65+
- `initial_p10`: scalar float.
66+
- `final_p01`: scalar float.
67+
- `final_p10`: scalar float.
68+
- `initial_loss`: scalar float.
69+
- `final_loss`: scalar float.
70+
- `loss_history`: NumPy array with length `config["max_steps"]`.
71+
- `target_expectations`: NumPy array with shape `(4, 13)`.
72+
- `fitted_expectations`: NumPy array with shape `(4, 13)`.
73+
74+
The solution may use any quantum software framework, but it must consume only the evaluator-provided configuration and return only this NumPy-format dictionary.
75+
76+
## Evaluation Interface
77+
78+
The evaluator file is `evaluate_4.py`. It dynamically imports a solution module selected by:
79+
80+
```bash
81+
python evaluate_4.py --solution solution_4
82+
```
83+
84+
The evaluator consumes only the returned result dictionary. It prints the true probabilities, initialized probabilities, fitted probabilities, absolute parameter errors, initial and final loss, expectation-table errors, trace-preserving error for the fitted Kraus operators, loss-history length, returned keys, and a target-versus-fitted observable table.
85+
86+
## Passing Criteria
87+
88+
A run is considered functionally successful when all of the following hold for the default 120-step configuration:
89+
90+
- `len(loss_history) == 120`.
91+
- `target_expectations.shape == (4, 13)` and `fitted_expectations.shape == (4, 13)`.
92+
- `final_loss < initial_loss`.
93+
- The absolute errors of both fitted probabilities are at most `1e-4`.
94+
- The fitted Kraus operators satisfy `max(abs(sum_a K_a^\dagger K_a - I)) <= 1e-8`.
95+
- All returned values are NumPy arrays or NumPy-compatible scalars.
96+
97+
The evaluator reports these metrics directly so another framework's `solution_4.py` can be compared without changing `evaluate_4.py`.
98+
99+
## TC-NG Baseline
100+
101+
The TensorCircuit-NG solution in `solution_4.py` can be evaluated with:
102+
103+
```bash
104+
python evaluate_4.py --solution solution_4
105+
```
106+
107+
Observed TensorCircuit-NG baseline with the default 120-step configuration:
108+
109+
- End-to-end solution time: `24.68s`.
110+
- Initial loss: `9.15254187e-03`.
111+
- Final loss: `1.92410443e-09`.
112+
- Fitted `p01`: `0.03398204`.
113+
- Fitted `p10`: `0.01099501`.
114+
- Max expectation absolute error: `1.97350979e-04`.
115+
- Trace-preserving error: `0.00000000e+00`.
116+
117+
## Implementation Hint
118+
119+
For a TensorCircuit-NG/JAX baseline, use `DMCircuit` with `apply_general_kraus` to insert the custom one-qubit channel after each entangling operation. The probes share one circuit structure and differ only in their initial product states, keeping the benchmark focused on the trainable channel. Keep the Kraus operators as differentiable tensor algebra, enforce positivity with sigmoid-parameterized probabilities, and verify trace preservation by contracting `sum_a K_a^\dagger K_a`.

examples/challenge_suite/solution_3.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,6 @@ def loss_fn(p):
178178
"final_loss": K.numpy(final_loss),
179179
"energy_density_history": K.numpy(K.stack(energy_density_history)),
180180
"success_probability_history": K.numpy(K.stack(success_probability_history)),
181-
"mean_log_probability_history": K.numpy(
182-
K.stack(mean_log_probability_history)
183-
),
181+
"mean_log_probability_history": K.numpy(K.stack(mean_log_probability_history)),
184182
"loss_history": K.numpy(K.stack(loss_history)),
185183
}

0 commit comments

Comments
 (0)