Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
validate_squander_vs_qiskit.py
Go to the documentation of this file.
1 """Mandatory micro-validation matrix for the exact observable path.
2 
3 Validates the required 1- to 3-qubit microcases against Qiskit Aer
4 density-matrix simulation using the frozen exact-observable contract:
5 `Re Tr(H*rho)`, density validity, trace preservation, and Hermitian-observable
6 consistency.
7 
8 Run with: python benchmarks/density_matrix/validate_squander_vs_qiskit.py
9 """
10 
11 from __future__ import annotations
12 
13 import argparse
14 import json
15 from pathlib import Path
16 import sys
17 
18 import numpy as np
19 import qiskit
20 import qiskit_aer
21 from qiskit.quantum_info import DensityMatrix as QiskitDensityMatrix
22 from qiskit.quantum_info import state_fidelity
23 
24 REPO_ROOT = Path(__file__).resolve().parents[2]
25 if str(REPO_ROOT) not in sys.path:
26  sys.path.insert(0, str(REPO_ROOT))
27 
28 from benchmarks.density_matrix.circuits import MANDATORY_MICROCASES_BY_QUBITS # noqa: E402
29 
30 PRIMARY_BACKEND = "density_matrix"
31 REFERENCE_BACKEND = "qiskit_aer_density_matrix"
32 SUITE_NAME = "mandatory_micro_validation"
33 ENERGY_ERROR_TOL = 1e-10
34 VALIDITY_TOL = 1e-10
35 TRACE_TOL = 1e-10
36 OBSERVABLE_IMAG_TOL = 1e-10
37 ARTIFACT_FILENAME = "micro_validation_bundle.json"
38 ARTIFACT_CORE_FIELDS = (
39  "suite_name",
40  "status",
41  "backend",
42  "reference_backend",
43  "thresholds",
44  "summary",
45  "software",
46  "cases",
47 )
48 
49 
50 def parse_microcase_operation(operation: str):
51  """Return machine-readable metadata for one microcase builder operation."""
52  if operation.startswith("U3("):
53  payload = operation[len("U3(") : -1].split(",")
54  return {
55  "kind": "gate",
56  "name": "U3",
57  "target_qbit": int(payload[0]),
58  "control_qbit": None,
59  "value": None,
60  "raw_operation": operation,
61  }
62 
63  if operation.startswith("CNOT("):
64  control, target = operation[len("CNOT(") : -1].split("->")
65  return {
66  "kind": "gate",
67  "name": "CNOT",
68  "target_qbit": int(target),
69  "control_qbit": int(control),
70  "value": None,
71  "raw_operation": operation,
72  }
73 
74  if operation.startswith("LocalDepol("):
75  target, value = operation[len("LocalDepol(") : -1].split(",")
76  return {
77  "kind": "noise",
78  "name": "local_depolarizing",
79  "target_qbit": int(target),
80  "control_qbit": None,
81  "value": float(value),
82  "raw_operation": operation,
83  }
84 
85  if operation.startswith("AD("):
86  target, value = operation[len("AD(") : -1].split(",")
87  return {
88  "kind": "noise",
89  "name": "amplitude_damping",
90  "target_qbit": int(target),
91  "control_qbit": None,
92  "value": float(value),
93  "raw_operation": operation,
94  }
95 
96  if operation.startswith("PD("):
97  target, value = operation[len("PD(") : -1].split(",")
98  return {
99  "kind": "noise",
100  "name": "phase_damping",
101  "target_qbit": int(target),
102  "control_qbit": None,
103  "value": float(value),
104  "raw_operation": operation,
105  }
106 
107  if operation.startswith("Depol("):
108  value = operation[len("Depol(") : -1]
109  return {
110  "kind": "noise",
111  "name": "depolarizing",
112  "target_qbit": None,
113  "control_qbit": None,
114  "value": float(value),
115  "raw_operation": operation,
116  }
117 
118  for gate_name in ("H", "X", "Y", "Z", "S", "T", "Sdg", "Tdg", "SX"):
119  prefix = f"{gate_name}("
120  if operation.startswith(prefix):
121  target = operation[len(prefix) : -1]
122  return {
123  "kind": "gate",
124  "name": gate_name,
125  "target_qbit": int(target),
126  "control_qbit": None,
127  "value": None,
128  "raw_operation": operation,
129  }
130 
131  if operation.startswith("CZ("):
132  control, target = operation[len("CZ(") : -1].split(",")
133  return {
134  "kind": "gate",
135  "name": "CZ",
136  "target_qbit": int(target),
137  "control_qbit": int(control),
138  "value": None,
139  "raw_operation": operation,
140  }
141 
142  raise ValueError(f"Unsupported microcase operation format: {operation}")
143 
144 
145 def build_microcase_operation_audit(case, operations):
146  """Summarize required gate/noise coverage and mixed-sequence order."""
147  operation_metadata = [parse_microcase_operation(operation) for operation in operations]
148  gate_operations = [
149  operation for operation in operation_metadata if operation["kind"] == "gate"
150  ]
151  noise_operations = [
152  operation for operation in operation_metadata if operation["kind"] == "noise"
153  ]
154  gate_sequence = [operation["name"] for operation in gate_operations]
155  noise_sequence = [operation["name"] for operation in noise_operations]
156  noise_targets = [operation["target_qbit"] for operation in noise_operations]
157  noise_values = [operation["value"] for operation in noise_operations]
158 
159  required_gate_coverage_pass = set(case["required_gate_family"]).issubset(
160  set(gate_sequence)
161  )
162  required_noise_model_coverage_pass = set(case["required_noise_models"]).issubset(
163  set(noise_sequence)
164  )
165  noise_sequence_match_pass = noise_sequence == case["required_noise_models"]
166  mixed_sequence_order_pass = (
167  noise_sequence_match_pass if case["case_kind"] == "mixed_sequence" else None
168  )
169  operation_audit_pass = (
170  required_gate_coverage_pass
171  and required_noise_model_coverage_pass
172  and noise_sequence_match_pass
173  )
174 
175  return {
176  "operation_metadata": operation_metadata,
177  "operation_count": len(operation_metadata),
178  "gate_operation_sequence": gate_sequence,
179  "noise_operation_sequence": noise_sequence,
180  "noise_operation_targets": noise_targets,
181  "noise_operation_values": noise_values,
182  "required_gate_coverage_pass": required_gate_coverage_pass,
183  "required_noise_model_coverage_pass": required_noise_model_coverage_pass,
184  "noise_sequence_match_pass": noise_sequence_match_pass,
185  "mixed_sequence_order_pass": mixed_sequence_order_pass,
186  "operation_audit_pass": operation_audit_pass,
187  }
188 
189 
190 def trace_distance(rho1: np.ndarray, rho2: np.ndarray) -> float:
191  """Compute trace distance: D(ρ,σ) = (1/2)||ρ - σ||_1."""
192  diff = rho1 - rho2
193  eigenvalues = np.linalg.eigvalsh(diff @ diff.conj().T)
194  return float(0.5 * np.sum(np.sqrt(np.maximum(eigenvalues, 0))))
195 
196 
197 def density_energy(hamiltonian: np.ndarray, density_matrix: np.ndarray):
198  """Return the real and imaginary parts of Tr(H*rho)."""
199  energy = np.trace(hamiltonian @ density_matrix)
200  return float(np.real(energy)), float(np.imag(energy))
201 
202 
204  return {
205  "absolute_energy_error": ENERGY_ERROR_TOL,
206  "rho_is_valid_tol": VALIDITY_TOL,
207  "trace_deviation": TRACE_TOL,
208  "observable_imag_abs": OBSERVABLE_IMAG_TOL,
209  "required_pass_rate": 1.0,
210  }
211 
212 
214  return {
215  "python": sys.version.split()[0],
216  "numpy": np.__version__,
217  "qiskit": getattr(qiskit, "__version__", "unknown"),
218  "qiskit_aer": getattr(qiskit_aer, "__version__", "unknown"),
219  }
220 
221 
222 def _microcase_base(case):
223  return {
224  "case_name": case["case_name"],
225  "status": "fail",
226  "backend": PRIMARY_BACKEND,
227  "reference_backend": REFERENCE_BACKEND,
228  "qbit_num": case["qbit_num"],
229  "case_kind": case["case_kind"],
230  "purpose": case["purpose"],
231  "required_gate_family": case["required_gate_family"],
232  "required_noise_models": case["required_noise_models"],
233  "hamiltonian": case["hamiltonian_metadata"],
234  }
235 
236 
237 def validate_microcase(case, verbose=True):
238  """Evaluate one mandatory micro-validation microcase."""
239  builder = case["builder_fn"]()
240  squander_rho = builder.run_squander()
241  qiskit_rho = builder.run_qiskit()
242  operations = list(builder.ops)
243  operation_audit = build_microcase_operation_audit(case, operations)
244 
245  squander_arr = np.asarray(squander_rho.to_numpy())
246  qiskit_arr = np.asarray(qiskit_rho)
247 
248  fidelity = float(
249  state_fidelity(
250  QiskitDensityMatrix(squander_arr), QiskitDensityMatrix(qiskit_arr)
251  )
252  )
253  max_diff = float(np.max(np.abs(squander_arr - qiskit_arr)))
254  rho_trace = squander_rho.trace()
255  trace_deviation = float(abs(rho_trace - 1.0))
256  density_valid = bool(squander_rho.is_valid(tol=VALIDITY_TOL))
257  squander_purity = float(np.real(np.trace(squander_arr @ squander_arr)))
258  qiskit_purity = float(np.real(np.trace(qiskit_arr @ qiskit_arr)))
259  state_distance = trace_distance(squander_arr, qiskit_arr)
260 
261  squander_energy_real, squander_energy_imag = density_energy(
262  case["hamiltonian_matrix"], squander_arr
263  )
264  reference_energy_real, reference_energy_imag = density_energy(
265  case["hamiltonian_matrix"], qiskit_arr
266  )
267  energy_error = float(abs(squander_energy_real - reference_energy_real))
268 
269  energy_pass = energy_error <= ENERGY_ERROR_TOL
270  density_valid_pass = density_valid
271  trace_pass = trace_deviation <= TRACE_TOL
272  observable_pass = abs(squander_energy_imag) <= OBSERVABLE_IMAG_TOL
273  state_comparison_status = (
274  "pass" if fidelity > 0.99999 else ("warn" if fidelity > 0.999 else "fail")
275  )
276  case_pass = (
277  energy_pass
278  and density_valid_pass
279  and trace_pass
280  and observable_pass
281  and operation_audit["operation_audit_pass"]
282  )
283 
284  result = _microcase_base(case)
285  result.update(
286  {
287  "status": "pass" if case_pass else "fail",
288  "parameter_vector": builder.get_parameter_vector().tolist(),
289  "operations": operations,
290  "state_fidelity": fidelity,
291  "trace_distance": state_distance,
292  "max_diff": max_diff,
293  "state_comparison_status": state_comparison_status,
294  "squander_purity": squander_purity,
295  "reference_purity": qiskit_purity,
296  "squander_trace_real": float(np.real(rho_trace)),
297  "squander_trace_imag": float(np.imag(rho_trace)),
298  "trace_deviation": trace_deviation,
299  "rho_is_valid": density_valid,
300  "rho_is_valid_tol": VALIDITY_TOL,
301  "squander_energy_real": squander_energy_real,
302  "squander_energy_imag": squander_energy_imag,
303  "reference_energy_real": reference_energy_real,
304  "reference_energy_imag": reference_energy_imag,
305  "absolute_energy_error": energy_error,
306  "energy_pass": energy_pass,
307  "density_valid_pass": density_valid_pass,
308  "trace_pass": trace_pass,
309  "observable_pass": observable_pass,
310  **operation_audit,
311  }
312  )
313 
314  if verbose:
315  print(
316  " {case_name:<38} [{backend} vs {reference}] "
317  "|ΔE|={energy_error:.3e} |Tr-1|={trace_dev:.3e} "
318  "|ImE|={imag:.3e} status={status}".format(
319  case_name=result["case_name"],
320  backend=result["backend"],
321  reference=result["reference_backend"],
322  energy_error=result["absolute_energy_error"],
323  trace_dev=result["trace_deviation"],
324  imag=abs(result["squander_energy_imag"]),
325  status=result["status"].upper(),
326  )
327  )
328 
329  return result
330 
331 
332 def capture_microcase(case, verbose=True):
333  """Capture validation output for one mandatory micro-validation microcase."""
334  try:
335  return validate_microcase(case, verbose=verbose)
336  except Exception as exc:
337  result = _microcase_base(case)
338  result.update(
339  {
340  "status": "fail",
341  "error_message": str(exc),
342  "energy_pass": False,
343  "density_valid_pass": False,
344  "trace_pass": False,
345  "observable_pass": False,
346  }
347  )
348  if verbose:
349  print(
350  " {case_name:<38} [{backend} vs {reference}] ERROR {message}".format(
351  case_name=result["case_name"],
352  backend=result["backend"],
353  reference=result["reference_backend"],
354  message=result["error_message"],
355  )
356  )
357  return result
358 
359 
360 def run_validation(verbose=True):
361  """Run the mandatory micro-validation micro-validation matrix."""
362  if verbose:
363  print("=" * 78)
364  print(
365  " micro-validation Micro-Validation [{} vs {}]".format(
366  PRIMARY_BACKEND, REFERENCE_BACKEND
367  )
368  )
369  print("=" * 78)
370 
371  results = []
372  for qbit_num in sorted(MANDATORY_MICROCASES_BY_QUBITS.keys()):
373  if verbose:
374  print(f"\n--- {qbit_num}-QUBIT MANDATORY MICROCASES ---", flush=True)
375  for case in MANDATORY_MICROCASES_BY_QUBITS[qbit_num]:
376  results.append(capture_microcase(case, verbose=verbose))
377  return results
378 
379 
381  passed = sum(1 for result in results if result["status"] == "pass")
382  total = len(results)
383  pass_rate = 0.0 if total == 0 else passed / total
384  bundle = {
385  "suite_name": SUITE_NAME,
386  "status": "pass" if pass_rate == 1.0 else "fail",
387  "backend": PRIMARY_BACKEND,
388  "reference_backend": REFERENCE_BACKEND,
389  "thresholds": build_threshold_metadata(),
390  "software": build_software_metadata(),
391  "summary": {
392  "total_cases": total,
393  "passed_cases": passed,
394  "failed_cases": total - passed,
395  "pass_rate": pass_rate,
396  },
397  "cases": results,
398  }
400  return bundle
401 
402 
404  missing_fields = [field for field in ARTIFACT_CORE_FIELDS if field not in bundle]
405  if missing_fields:
406  raise ValueError(
407  "Artifact bundle is missing required fields: {}".format(
408  ", ".join(missing_fields)
409  )
410  )
411 
412 
413 def write_artifact_bundle(output_path: Path, bundle):
415  output_path.parent.mkdir(parents=True, exist_ok=True)
416  output_path.write_text(json.dumps(bundle, indent=2) + "\n", encoding="utf-8")
417 
418 
419 def print_summary(results):
420  """Print micro-validation summary."""
421  print("\n" + "=" * 78)
422  print(" SUMMARY")
423  print("=" * 78)
424 
425  for result in results:
426  details = []
427  if not result.get("energy_pass", False):
428  details.append("energy")
429  if not result.get("density_valid_pass", False):
430  details.append("density")
431  if not result.get("trace_pass", False):
432  details.append("trace")
433  if not result.get("observable_pass", False):
434  details.append("imag")
435  if "error_message" in result:
436  details.append("error")
437 
438  print(
439  " {case_name:<38} [{backend} vs {reference}] "
440  "|ΔE|={energy_error:.3e} status={status}{detail_suffix}".format(
441  case_name=result["case_name"],
442  backend=result["backend"],
443  reference=result["reference_backend"],
444  energy_error=result.get("absolute_energy_error", float("nan")),
445  status=result["status"].upper(),
446  detail_suffix="" if not details else " (" + ",".join(details) + ")",
447  )
448  )
449 
450  print("\n" + "-" * 78)
451  total = len(results)
452  passed = sum(1 for result in results if result["status"] == "pass")
453  print(f" Total: {passed}/{total} mandatory microcases passed")
454 
455  if passed == total:
456  print("\n ALL TESTS PASSED - micro-validation mandatory micro-validation gate is closed.")
457  else:
458  print("\n Some mandatory microcases failed - micro-validation is not yet closed.")
459 
460  print("=" * 78)
461 
462 
463 def main():
464  parser = argparse.ArgumentParser(description=__doc__)
465  parser.add_argument(
466  "--output-dir",
467  type=Path,
468  default=None,
469  help="Optional directory for the micro-validation JSON artifact bundle.",
470  )
471  args = parser.parse_args()
472 
473  results = run_validation()
474  bundle = build_artifact_bundle(results)
475  print_summary(results)
476 
477  if args.output_dir is not None:
478  write_artifact_bundle(args.output_dir / ARTIFACT_FILENAME, bundle)
479 
480  if bundle["status"] != "pass":
481  raise SystemExit(1)
482 
483 
484 if __name__ == "__main__":
485  main()
Definition: split.py:1
def validate_microcase(case, verbose=True)
def capture_microcase(case, verbose=True)
def build_microcase_operation_audit(case, operations)