From 1bef3507098792532bc722c8bcf7ed5681c68f7f Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Thu, 2 Apr 2026 20:06:03 +0000 Subject: [PATCH 01/21] initial implementation of sparse isometry --- examples/random_wavefunction.py | 482 +++++++ examples/state_prep_energy.ipynb | 1231 ++++++++++++++--- .../src/qdk_chemistry/algorithms/registry.py | 4 + .../algorithms/state_preparation/__init__.py | 4 + .../state_preparation/sparse_isometry.py | 572 ++++---- .../sparse_isometry_binary_encoding.py | 303 ++++ .../qdk_chemistry/utils/binary_encoding.py | 1114 +++++++++++++++ .../qdk_chemistry/utils/qsharp/__init__.py | 53 +- .../qdk_chemistry/utils/qsharp/qsharp.json | 11 + .../utils/qsharp/src/BinaryEncoding.qs | 323 +++++ .../qsharp/{ => src}/ControlledPauliExp.qs | 0 .../{ => src}/IterativePhaseEstimation.qs | 0 .../qsharp/{ => src}/MeasurementBasis.qs | 0 .../qsharp/{ => src}/StatePreparation.qs | 24 +- python/tests/test_circuit.py | 2 +- python/tests/test_state_preparation.py | 93 +- 16 files changed, 3627 insertions(+), 589 deletions(-) create mode 100644 examples/random_wavefunction.py create mode 100644 python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py create mode 100644 python/src/qdk_chemistry/utils/binary_encoding.py create mode 100644 python/src/qdk_chemistry/utils/qsharp/qsharp.json create mode 100644 python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs rename python/src/qdk_chemistry/utils/qsharp/{ => src}/ControlledPauliExp.qs (100%) rename python/src/qdk_chemistry/utils/qsharp/{ => src}/IterativePhaseEstimation.qs (100%) rename python/src/qdk_chemistry/utils/qsharp/{ => src}/MeasurementBasis.qs (100%) rename python/src/qdk_chemistry/utils/qsharp/{ => src}/StatePreparation.qs (87%) diff --git a/examples/random_wavefunction.py b/examples/random_wavefunction.py new file mode 100644 index 000000000..74cd5172b --- /dev/null +++ b/examples/random_wavefunction.py @@ -0,0 +1,482 @@ +"""Random wavefunction generation for benchmarking state preparation. + +This module generates physically motivated random wavefunctions by +exciting electrons from a Hartree-Fock reference determinant. It is +intended for benchmarking sparse-isometry and binary-encoding circuits +where the full qdk-chemistry pipeline is not needed. + +Public API +---------- +generate_determinants_matrix + Generate a ``(n_dets, 2*n_orbitals)`` matrix of random Slater determinants. +generate_sparse_isometry_matrix + Generate a ``(2*n_orbitals, n_dets)`` binary matrix in the format expected + by the sparse isometry state preparation routines. +generate_sparse_isometry_matrices + Same as above, for multiple determinant counts from a single pool. +generate_random_wavefunction + Generate a :class:`~qdk_chemistry.data.Wavefunction` from random excitations. +determinants_to_config_strings + Convert a determinant matrix to configuration strings. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from typing import Optional + +import numpy as np +from qdk_chemistry.data import ( + BasisSet, + CasWavefunctionContainer, + Configuration, + Orbitals, + OrbitalType, + Shell, + Wavefunction, +) + +# --------------------------------------------------------------------------- +# Determinant generation +# --------------------------------------------------------------------------- + + +def _hf_determinant(n_alpha: int, n_beta: int, n_orbitals: int) -> np.ndarray: + """Build the Hartree-Fock reference determinant. + + Layout: [alpha_0, alpha_1, ..., alpha_{n_orbitals-1}, + beta_0, beta_1, ..., beta_{n_orbitals-1}] + + The HF state fills the lowest orbitals for each spin channel. + """ + det = np.zeros(2 * n_orbitals, dtype=np.int8) + det[:n_alpha] = 1 # alpha electrons in lowest orbitals + det[n_orbitals : n_orbitals + n_beta] = 1 # beta electrons in lowest orbitals + return det + + +def _random_excitation( + det: np.ndarray, + n_orbitals: int, + rng: np.random.Generator, + max_excitation_order: Optional[int] = None, +) -> Optional[np.ndarray]: + """Generate a single random excitation from a reference determinant. + + Independently applies excitations in the alpha and beta spin channels. + The excitation order is randomly chosen between 1 and max_excitation_order. + + Returns None if the excitation produces a duplicate of the input. + """ + new_det = det.copy() + + # Process alpha and beta channels independently + for channel_start in (0, n_orbitals): + channel = det[channel_start : channel_start + n_orbitals] + occupied = np.where(channel == 1)[0] + virtual = np.where(channel == 0)[0] + + if len(occupied) == 0 or len(virtual) == 0: + continue + + max_exc = min(len(occupied), len(virtual)) + if max_excitation_order is not None: + max_exc = min(max_exc, max_excitation_order) + + # Randomly choose excitation order for this channel (0 means no excitation) + order = rng.integers(0, max_exc + 1) + if order == 0: + continue + + occ_indices = rng.choice(occupied, size=order, replace=False) + vir_indices = rng.choice(virtual, size=order, replace=False) + + new_det[channel_start + occ_indices] = 0 + new_det[channel_start + vir_indices] = 1 + + if np.array_equal(new_det, det): + return None + + return new_det + + +def generate_determinants_matrix( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, + max_excitation_order: Optional[int] = None, + n_alpha: Optional[int] = None, + n_beta: Optional[int] = None, + include_hf: bool = True, +) -> np.ndarray: + """Generate a matrix of Slater determinants by random excitation from HF. + + Each row is a determinant represented as a binary occupation vector over + 2*n_orbitals spin-orbitals (alpha block followed by beta block). + + This function does NOT enumerate the full determinant space; instead it + randomly samples excitations, making it suitable for large systems where + C(n_orbitals, n_electrons) is intractable. + + Args: + n_electrons: Total number of electrons. + n_orbitals: Number of spatial orbitals (spin-orbitals = 2 * n_orbitals). + n_dets: Number of determinants to generate (including HF if + ``include_hf`` is True). + seed: Random seed for reproducibility. + max_excitation_order: Maximum excitation rank per spin channel. + None means up to the maximum possible. + n_alpha: Number of alpha electrons. Defaults to ``n_electrons // 2``. + n_beta: Number of beta electrons. Defaults to + ``n_electrons - n_alpha``. + include_hf: Whether to always include the HF determinant as the first + row. + + Returns: + np.ndarray of shape ``(n_dets, 2 * n_orbitals)`` with entries 0 or 1. + + Raises: + ValueError: If inputs are inconsistent. + """ + if n_alpha is None: + n_alpha = n_electrons // 2 + if n_beta is None: + n_beta = n_electrons - n_alpha + + if n_alpha + n_beta != n_electrons: + raise ValueError( + f"n_alpha ({n_alpha}) + n_beta ({n_beta}) != n_electrons ({n_electrons})" + ) + if n_alpha > n_orbitals or n_beta > n_orbitals: + raise ValueError( + f"Cannot place {n_alpha} alpha or {n_beta} beta electrons " + f"in {n_orbitals} orbitals" + ) + if n_dets < 1: + raise ValueError("n_dets must be at least 1") + + from math import comb + + max_possible = comb(n_orbitals, n_alpha) * comb(n_orbitals, n_beta) + if n_dets > max_possible: + raise ValueError( + f"Requested {n_dets} determinants but the total space has only " + f"{max_possible}" + ) + + rng = np.random.default_rng(seed) + hf = _hf_determinant(n_alpha, n_beta, n_orbitals) + + seen: set[bytes] = set() + dets: list[np.ndarray] = [] + + if include_hf: + dets.append(hf) + seen.add(hf.tobytes()) + + max_attempts = n_dets * 200 + attempts = 0 + + while len(dets) < n_dets and attempts < max_attempts: + attempts += 1 + new_det = _random_excitation(hf, n_orbitals, rng, max_excitation_order) + if new_det is None: + continue + key = new_det.tobytes() + if key not in seen: + seen.add(key) + dets.append(new_det) + + if len(dets) < n_dets: + raise RuntimeError( + f"Only generated {len(dets)}/{n_dets} unique determinants after " + f"{max_attempts} attempts. Try increasing max_excitation_order or " + f"reducing n_dets." + ) + + return np.array(dets, dtype=np.int8) + + +# --------------------------------------------------------------------------- +# Sparse isometry matrix generation +# --------------------------------------------------------------------------- + + +def _bk_transformation_matrix(n: int) -> np.ndarray: + """Build the n x n Bravyi-Kitaev transformation matrix. + + The BK transformation converts an occupation-number vector f (JW basis) + to a BK qubit vector b via b = B @ f (mod 2). + + The matrix is built recursively: + + B_1 = [[1]] + B_{2k} = [[B_k, 0 ], + [S_k, B_k]] + + where S_k has its last row all 1s (rest zeros). For non-power-of-2 + sizes we pad to the next power of 2 and truncate. + """ + n_padded = 1 + while n_padded < n: + n_padded *= 2 + + def _build(size: int) -> np.ndarray: + if size == 1: + return np.array([[1]], dtype=np.int8) + half = size // 2 + b_half = _build(half) + top = np.hstack([b_half, np.zeros((half, half), dtype=np.int8)]) + s_k = np.zeros((half, half), dtype=np.int8) + s_k[half - 1, :] = 1 + bottom = np.hstack([s_k, b_half]) + return np.vstack([top, bottom]) + + full = _build(n_padded) + return full[:n, :n] + + +def generate_sparse_isometry_matrix( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, + max_excitation_order: Optional[int] = None, + n_alpha: Optional[int] = None, + n_beta: Optional[int] = None, + include_hf: bool = True, + encoding: str = "jordan-wigner", +) -> np.ndarray: + """Generate a binary matrix in the sparse isometry format. + + Output shape is ``(2 * n_orbitals, n_dets)`` where each column is a + determinant bitstring and each row is a qubit (spin-orbital). + + Args: + n_electrons: Total number of electrons. + n_orbitals: Number of spatial orbitals. + n_dets: Number of determinants to generate. + seed: Random seed for reproducibility. + max_excitation_order: Maximum excitation rank per spin channel. + n_alpha: Number of alpha electrons. + n_beta: Number of beta electrons. + include_hf: Whether to include the HF determinant as the first column. + encoding: ``"jordan-wigner"`` or ``"bravyi-kitaev"``. + + Returns: + np.ndarray of shape ``(2 * n_orbitals, n_dets)`` with entries 0 or 1. + """ + if encoding not in ("jordan-wigner", "bravyi-kitaev"): + raise ValueError( + f"Unknown encoding '{encoding}'. " + "Supported: 'jordan-wigner', 'bravyi-kitaev'" + ) + + det_matrix = generate_determinants_matrix( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=n_dets, + seed=seed, + max_excitation_order=max_excitation_order, + n_alpha=n_alpha, + n_beta=n_beta, + include_hf=include_hf, + ) + + # det_matrix is (n_dets, 2*n_orbitals) with [alpha|beta] layout. + # Qubit order q[0]..q[2N-1] = alpha_0 .. alpha_{N-1}, beta_0 .. beta_{N-1} + # which matches det_matrix columns — just transpose. + matrix = det_matrix.T.astype(np.int8) + + if encoding == "bravyi-kitaev": + bk = _bk_transformation_matrix(2 * n_orbitals) + matrix = np.mod(bk @ matrix, 2).astype(np.int8) + + return matrix + + +def generate_sparse_isometry_matrices( + n_electrons: int, + n_orbitals: int, + det_counts: list[int], + seed: int = 0, + max_excitation_order: Optional[int] = None, + n_alpha: Optional[int] = None, + n_beta: Optional[int] = None, + include_hf: bool = True, + encoding: str = "jordan-wigner", +) -> dict[int, np.ndarray]: + """Generate sparse isometry matrices for multiple determinant counts. + + Generates the pool once for ``max(det_counts)`` and slices prefixes. + + Returns: + Dict mapping each count to an ``(2*n_orbitals, count)`` array. + """ + max_dets = max(det_counts) + full_matrix = generate_sparse_isometry_matrix( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=max_dets, + seed=seed, + max_excitation_order=max_excitation_order, + n_alpha=n_alpha, + n_beta=n_beta, + include_hf=include_hf, + encoding=encoding, + ) + return {k: full_matrix[:, :k].copy() for k in sorted(det_counts)} + + +# --------------------------------------------------------------------------- +# Configuration string helpers +# --------------------------------------------------------------------------- + + +def determinants_to_config_strings(matrix: np.ndarray, n_orbitals: int) -> list[str]: + """Convert a determinant matrix to configuration strings. + + Convention: '2' = doubly occupied, 'u' = alpha only, + 'd' = beta only, '0' = empty. + + Args: + matrix: Shape ``(n_dets, 2 * n_orbitals)`` binary matrix. + n_orbitals: Number of spatial orbitals. + + Returns: + List of configuration strings, one per determinant. + """ + configs = [] + for det in matrix: + alpha = det[:n_orbitals] + beta = det[n_orbitals:] + chars = [] + for a, b in zip(alpha, beta): + if a == 1 and b == 1: + chars.append("2") + elif a == 1 and b == 0: + chars.append("u") + elif a == 0 and b == 1: + chars.append("d") + else: + chars.append("0") + configs.append("".join(chars)) + return configs + + +# --------------------------------------------------------------------------- +# Wavefunction generation +# --------------------------------------------------------------------------- + + +def create_test_basis_set(num_atomic_orbitals, name="test-basis", structure=None): + """Create a test basis set with the specified number of atomic orbitals. + + Args: + num_atomic_orbitals: Number of atomic orbitals to generate + name: Name for the basis set + structure: a structure to attach + + Returns: + qdk_chemistry.data.BasisSet: A valid basis set for testing + + """ + shells = [] + atom_index = 0 + functions_created = 0 + + # Create shells to reach the desired number of atomic orbitals + while functions_created < num_atomic_orbitals: + remaining = num_atomic_orbitals - functions_created + + if remaining >= 3: + # Add a P shell (3 functions: Px, Py, Pz) + exps = np.array([1.0, 0.5]) + coefs = np.array([0.6, 0.4]) + shell = Shell(atom_index, OrbitalType.P, exps, coefs) + shells.append(shell) + functions_created += 3 + elif remaining >= 1: + # Add S shells for remaining functions (1 function each) + for _ in range(remaining): + exps = np.array([1.0]) + coefs = np.array([1.0]) + shell = Shell(atom_index, OrbitalType.S, exps, coefs) + shells.append(shell) + functions_created += 1 + if structure is not None: + return BasisSet(name, shells, structure) + return BasisSet(name, shells) + + +def create_test_orbitals(num_orbitals: int): + """Helper: construct Orbitals immutably with identity coeffs and occupations. + + Occupations are set in restricted form as total occupancy per MO (0/1/2). + """ + coeffs = np.eye(num_orbitals) + basis_set = create_test_basis_set(num_orbitals) + return Orbitals(coeffs, None, None, basis_set) + + +def generate_random_wavefunction( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, + max_excitation_order: Optional[int] = None, + n_alpha: Optional[int] = None, + n_beta: Optional[int] = None, + include_hf: bool = True, +) -> "Wavefunction": + """Generate a random :class:`~qdk_chemistry.data.Wavefunction`. + + Follows the same construction pattern as the ``wavefunction_4e4o`` + test fixture in ``conftest.py``: + + 1. Generate determinants via random excitation from HF. + 2. Convert to :class:`~qdk_chemistry.data.Configuration` strings. + 3. Assign random normalised coefficients. + 4. Wrap in ``CasWavefunctionContainer`` → ``Wavefunction``. + + Args: + n_electrons: Total number of electrons. + n_orbitals: Number of spatial orbitals. + n_dets: Number of determinants. + seed: Random seed for reproducibility. + max_excitation_order: Maximum excitation rank per spin channel. + n_alpha: Number of alpha electrons. + n_beta: Number of beta electrons. + include_hf: Whether to include the HF determinant. + + Returns: + A normalised :class:`~qdk_chemistry.data.Wavefunction`. + """ + + det_matrix = generate_determinants_matrix( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=n_dets, + seed=seed, + max_excitation_order=max_excitation_order, + n_alpha=n_alpha, + n_beta=n_beta, + include_hf=include_hf, + ) + + config_strings = determinants_to_config_strings(det_matrix, n_orbitals) + dets = [Configuration(s) for s in config_strings] + + # Random normalised coefficients (offset seed to avoid correlation + # with determinant generation) + rng = np.random.default_rng(seed + 999) + raw = rng.standard_normal(n_dets) + coeffs = raw / np.linalg.norm(raw) + + # Identity MO coefficients — sufficient for state-preparation benchmarks + orbitals = create_test_orbitals(n_orbitals) + + container = CasWavefunctionContainer(coeffs, dets, orbitals) + return Wavefunction(container) diff --git a/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index a69fef090..6b1de8904 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -1,76 +1,29 @@ { "cells": [ - { - "cell_type": "markdown", - "id": "115ee185", - "metadata": {}, - "source": [ - "# Using `qdk-chemistry` for multi-reference quantum chemistry state preparation and energy estimation\n", - "\n", - "This notebook demonstrates an end-to-end multi-configurational quantum chemistry workflow using `qdk-chemistry`.\n", - "It covers molecule loading and visualization, self-consistent-field (SCF) calculation, active-space selection, multi-configurational wavefunction generation, quantum state-preparation circuit construction, and measurement circuits for energy estimation.\n", - "\n", - "**Prerequisites:** In addition to [installing `qdk-chemistry`](https://github.com/microsoft/qdk-chemistry/blob/main/INSTALL.md), you will need to install the `jupyter` extra to run this notebook:\n", - "\n", - "```bash\n", - "pip install 'qdk-chemistry[jupyter]'\n", - "```\n", - "\n", - "This installs the additional dependencies required by this notebook (ipykernel, pandas, and pyscf).\n", - "\n", - "---\n", - "\n", - "In many molecular systems—such as bond dissociation or transition-metal complexes—a single electronic configuration cannot describe the true electronic structure.\n", - "These multi-configurational systems exhibit strong electron correlation that challenges mean-field and single-determinant methods like [Hartree–Fock](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method) or standard [coupled cluster theory](https://en.wikipedia.org/wiki/Coupled_cluster).\n", - "\n", - "While classical multi-configurational approaches can capture these effects, their computational cost grows exponentially with system size.\n", - "Quantum computers offer a complementary route: they can represent superpositions of many configurations natively and solve these problems with polynomial scaling.\n", - "\n", - "However, near-term fault-tolerant quantum hardware is still in the early stages of growth and scaling.\n", - "To use it effectively, we must compress and optimize chemistry problems before they reach the quantum device.\n", - "Classical methods enable this by identifying essential orbitals through active-space selection, generating approximate wavefunctions for state preparation, and supplying data to optimize quantum circuits for energy estimation.\n", - "\n", - "This notebook focuses on state preparation, where a multi-configurational wavefunction from classical computation is transformed into a quantum circuit.\n", - "State preparation is central to quantum chemistry algorithms such as [Quantum Phase Estimation (QPE)](https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) and also serves as a practical hardware benchmark: preparing complex multi-configurational states tests the fidelity and coherence of quantum hardware.\n", - "\n", - "In the example below, we show how to generate and optimize state preparation circuits, from active-space selection to energy measurement, demonstrating how chemical insight can reduce quantum resource requirements for near-term devices." - ] - }, - { - "cell_type": "markdown", - "id": "d0a5a02f", - "metadata": {}, - "source": [ - "## Loading and visualizing the molecular structure\n", - "\n", - "For this example, we will use the benzene diradical molecule.\n", - "The benzene diradical has two unpaired electrons, making it a good candidate for multi-reference quantum chemistry methods.\n", - "This molecule is also an important intermediate in the [Bergman cyclization reaction](https://en.wikipedia.org/wiki/Bergman_cyclization), a popular reaction in synthetic organic chemistry.\n", - "\n", - "The molecular structure is provided in the [XYZ file format](https://en.wikipedia.org/wiki/XYZ_file_format).\n", - "This cell demonstrates how to load the molecule and visualize its structure." - ] - }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "5436376a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n let rules = [\n {\n token: \"comment\",\n regex: /(\\/\\/).*/,\n beginWord: false,\n },\n {\n token: \"string\",\n regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n beginWord: false,\n },\n {\n token: \"keyword\",\n regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(not|and|or)\\b|(w/)`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(=)|(!)|(<)|(>)|(\\+)|(-)|(\\*)|(/)|(\\^)|(%)|(\\|)|(&&&)|(~~~)|(\\.\\.\\.)|(\\.\\.)|(\\?)`,\n beginWord: false,\n },\n {\n token: \"meta\",\n regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n beginWord: true,\n },\n {\n token: \"atom\",\n regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n beginWord: true,\n },\n ];\n let simpleRules = [];\n for (let rule of rules) {\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(rule.regex, \"g\"),\n sol: rule.beginWord,\n });\n if (rule.beginWord) {\n // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n sol: false,\n });\n }\n }\n\n // Register the mode defined above with CodeMirror\n window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n reg: [/^%%qsharp/],\n };\n\n // Force re-highlighting of all cells the first time this code runs\n for (const cell of window.Jupyter.notebook.get_cells()) {\n cell.auto_highlight();\n }\n });\n}\n", + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from pathlib import Path\n", "\n", - "from qdk.widgets import MoleculeViewer\n", - "\n", "from qdk_chemistry.data import Structure\n", "\n", "# Read molecular structure from XYZ file\n", "structure = Structure.from_xyz_file(\n", " Path(\".\") / \"data/benzene_diradical.structure.xyz\"\n", - ")\n", - "\n", - "# Visualize the molecular structure\n", - "display(MoleculeViewer(molecule_data=structure.to_xyz()))" + ")" ] }, { @@ -87,10 +40,80 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "75d71220", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-04-02 17:15:23.081763] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] mol: atoms=10, electrons=40, n_ecp_electrons=0, charge=0, multiplicity=1, spin(2S)=0, alpha=20, beta=20\n", + "[2026-04-02 17:15:23.081813] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] restricted=true, basis=cc-pvdz, pure=true, num_atomic_orbitals=104, density_threshold=1.00e-05, og_threshold=1.00e-07\n", + "[2026-04-02 17:15:23.081816] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] fock_alg=TradJK\n", + "[2026-04-02 17:15:23.081818] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] eri_threshold=1.00e-12, shell_pair_threshold=1.00e-12\n", + "[2026-04-02 17:15:23.081821] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] world_size=1, omp_get_max_threads=8\n", + "[2026-04-02 17:15:23.238719] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", + "[2026-04-02 17:15:23.487542] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 000: E=-2.322479405965371e+02, DE=-2.322479405965371e+02, |DP|=5.849507414288221e-02, |DG|=2.118472909388689e-02, \n", + "[2026-04-02 17:15:23.563530] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", + "[2026-04-02 17:15:24.075657] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 001: E=-2.291813329598724e+02, DE=+3.066607636664656e+00, |DP|=3.126260151046291e-02, |DG|=7.249889224905371e-04, \n", + "[2026-04-02 17:15:24.156506] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", + "[2026-04-02 17:15:24.603974] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 002: E=-2.292553237397959e+02, DE=-7.399077992351977e-02, |DP|=9.803468311889503e-03, |DG|=3.195504248892452e-04, \n", + "[2026-04-02 17:15:25.115811] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 003: E=-2.292640447846453e+02, DE=-8.721044849323789e-03, |DP|=1.996996960235905e-03, |DG|=7.614580500099797e-05, \n", + "[2026-04-02 17:15:25.589662] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 004: E=-2.292653960205162e+02, DE=-1.351235870913570e-03, |DP|=1.704011448517370e-03, |DG|=1.743802168804288e-05, \n", + "[2026-04-02 17:15:26.072640] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 005: E=-2.292654536894343e+02, DE=-5.766891808889341e-05, |DP|=3.921150071152143e-04, |DG|=8.892118464230874e-06, \n", + "[2026-04-02 17:15:26.072707] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:diis_gdm] Switching from DIIS to GDM at step 6 (delta_energy=-5.7668918088893406e-05, max_diis_step=50)\n", + "[2026-04-02 17:15:26.084450] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:gdm] No history available, using initial Hessian inverse\n", + "[2026-04-02 17:15:27.355966] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 006: E=-2.292654599091643e+02, DE=-6.219730039447313e-06, |DP|=3.590797971823754e-05, |DG|=4.330434592709749e-06, \n", + "[2026-04-02 17:15:28.942314] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 007: E=-2.292654625017283e+02, DE=-2.592563987491303e-06, |DP|=4.415900623359871e-05, |DG|=3.447236387212594e-06, \n", + "[2026-04-02 17:15:30.650865] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 008: E=-2.292654634735950e+02, DE=-9.718667115521384e-07, |DP|=3.507633868007982e-05, |DG|=2.008776644399891e-06, \n", + "[2026-04-02 17:15:31.538733] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 009: E=-2.292654637022202e+02, DE=-2.286252538397093e-07, |DP|=1.629217307319500e-05, |DG|=4.675644360142092e-07, \n", + "[2026-04-02 17:15:32.278392] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 010: E=-2.292654637160156e+02, DE=-1.379532932332950e-08, |DP|=1.732546445848254e-06, |DG|=1.236880060990507e-07, \n", + "[2026-04-02 17:15:33.087694] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 011: E=-2.292654637177791e+02, DE=-1.763510226737708e-09, |DP|=9.451803020885315e-07, |DG|=1.114090723350550e-07, \n", + "[2026-04-02 17:15:34.220548] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 012: E=-2.292654637185898e+02, DE=-8.107008397928439e-10, |DP|=8.882188359183630e-07, |DG|=4.954809240302295e-08, \n", + "[2026-04-02 17:15:34.766661] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", + "[2026-04-02 17:15:35.255372] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] SCF converged: steps=13, E=-229.265463718590\n", + "-----------------------------------------------------------------\n", + "Nuclear Repulsion Energy = 184.689150054839\n", + "One-Electron Energy = -673.763662885958\n", + "Two-Electron Energy = 259.809049112529\n", + "DFT Exchange-Correlation Energy = 0.000000000000\n", + "Total Energy = -229.265463718590\n", + "\n", + "Total Dipole (a.u.)\n", + " X_ = -0.000000000000\n", + " Y = 0.000000000003\n", + " Z = 0.000000000000\n", + "\n", + "Mulliken Charges (a.u.)\n", + " Atom 0 Z= 6 -9.54308573e-02\n", + " Atom 1 Z= 6 -2.60256781e-02\n", + " Atom 2 Z= 6 -2.60256781e-02\n", + " Atom 3 Z= 6 -9.54308573e-02\n", + " Atom 4 Z= 6 -2.60256781e-02\n", + " Atom 5 Z= 6 -2.60256781e-02\n", + " Atom 6 Z= 1 7.37411067e-02\n", + " Atom 7 Z= 1 7.37411067e-02\n", + " Atom 8 Z= 1 7.37411067e-02\n", + " Atom 9 Z= 1 7.37411067e-02\n", + "-----------------------------------------------------------------\n", + "SCF energy is -229.265 Hartree\n", + "SCF Orbitals:\n", + " Orbitals Summary:\n", + " AOs: 104\n", + " MOs: 104\n", + " Type: Restricted\n", + " Has overlap: Yes\n", + " Has basis set: Yes\n", + " Valid: Yes\n", + " Has active space: Yes\n", + " Active Orbitals: α=104, β=104\n", + " Has inactive space: No\n", + " Virtual Orbitals: α=0, β=0\n", + "\n" + ] + } + ], "source": [ "from qdk_chemistry.algorithms import create\n", "\n", @@ -126,10 +149,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "2f5ea942", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active Space Orbitals:\n", + " Orbitals Summary:\n", + " AOs: 104\n", + " MOs: 104\n", + " Type: Restricted\n", + " Has overlap: Yes\n", + " Has basis set: Yes\n", + " Valid: Yes\n", + " Has active space: Yes\n", + " Active Orbitals: α=6, β=6\n", + " Has inactive space: Yes\n", + " Inactive Orbitals: α=17, β=17\n", + " Virtual Orbitals: α=81, β=81\n", + "\n", + "[2026-04-02 17:15:35.349861] [info] [qdk:chemistry:algorithms:microsoft:active_space:valence_active_space] ValenceActiveSpaceSelector::Starting active space selection.\n", + "[2026-04-02 17:15:35.350027] [info] [qdk:chemistry:algorithms:microsoft:active_space:valence_active_space] ValenceActiveSpaceSelector::Selected active space of 6 orbitals: 17, 18, 19, 20, 21, 22\n" + ] + } + ], "source": [ "# Select active space (6 electrons in 6 orbitals for benzene diradical) to choose most chemically relevant orbitals\n", "active_space_selector = create(\"active_space_selector\", algorithm_name=\"qdk_valence\",\n", @@ -150,28 +196,6 @@ "The drop-down menu provides the ability to select different occupied and virtual orbitals in the active space to visualize their shapes, while the isovalue slider adjusts the surface representation of the orbitals for different electron density levels.\n" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d8850fc", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.utils.cubegen import generate_cubefiles_from_orbitals\n", - "\n", - "# Generate cube files for the active orbitals\n", - "cube_data = generate_cubefiles_from_orbitals(\n", - " orbitals=active_orbitals,\n", - " grid_size=(40, 40, 40),\n", - " margin=10.0,\n", - " indices=active_orbitals.get_active_space_indices()[0],\n", - " label_maker=lambda p: f\"{'occupied' if p < 20 else 'virtual'}_{p + 1:04d}\"\n", - ")\n", - "\n", - "# Visualize the molecular orbitals together with the structure\n", - "MoleculeViewer(molecule_data=structure.to_xyz(), cube_data=cube_data)" - ] - }, { "cell_type": "markdown", "id": "f8b8e498", @@ -194,10 +218,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "980dae08", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active Space Hamiltonian:\n", + " Hamiltonian Summary:\n", + " Type: Hermitian\n", + " Restrictedness: Restricted\n", + " Active Orbitals: 6\n", + " Total Orbitals: 104\n", + " Core Energy: -223.094600\n", + " Integral Statistics:\n", + " One-body Integrals (alpha): 36 (larger than 0.000001: 6)\n", + " Two-body Integrals (aaaa): 1296 (larger than 0.000001: 168)\n", + "\n", + "[2026-04-02 17:15:37.073] [ci_solver] [info] [Selected CI Solver]:\n", + "[2026-04-02 17:15:37.073] [ci_solver] [info] NDETS = 400, MATEL_TOL = 2.22045e-16, RES_TOL = 1.00000e-06, MAX_SUB = 200\n", + "[2026-04-02 17:15:37.084] [ci_solver] [info] NNZ = 22448, H_DUR = 1.05001e+01 ms\n", + "[2026-04-02 17:15:37.084] [ci_solver] [info] HMEM_LOC = 6.54e-04 GiB\n", + "[2026-04-02 17:15:37.084] [ci_solver] [info] H_SPARSE = 1.40e+01%\n", + "[2026-04-02 17:15:37.084] [ci_solver] [info] * Will generate identity guess\n", + "[2026-04-02 17:15:37.084] [davidson] [info] [Davidson Eigensolver]:\n", + "[2026-04-02 17:15:37.084] [davidson] [info] N = 400, MAX_M = 200, RES_TOL = 1.00000e-06\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 1, LAM(0) = -6.272181531114e+00, RNORM = 1.508328251603e-01\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 2, LAM(0) = -6.315584336020e+00, RNORM = 7.639642777831e-02\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 3, LAM(0) = -6.323257851542e+00, RNORM = 2.095312033898e-02\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 4, LAM(0) = -6.323885609955e+00, RNORM = 7.536714097302e-03\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 5, LAM(0) = -6.323957966169e+00, RNORM = 1.432015296461e-03\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 6, LAM(0) = -6.323963692872e+00, RNORM = 7.115396705615e-04\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 7, LAM(0) = -6.323964619315e+00, RNORM = 2.254315103486e-04\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 8, LAM(0) = -6.323964690443e+00, RNORM = 4.475298011079e-05\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 9, LAM(0) = -6.323964694013e+00, RNORM = 1.094187112506e-05\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 10, LAM(0) = -6.323964694331e+00, RNORM = 4.039726192293e-06\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 11, LAM(0) = -6.323964694356e+00, RNORM = 1.067837448925e-06\n", + "[2026-04-02 17:15:37.084] [davidson] [info] iter = 12, LAM(0) = -6.323964694358e+00, RNORM = 2.304002722527e-07\n", + "[2026-04-02 17:15:37.084] [davidson] [info] Davidson Converged!\n", + "[2026-04-02 17:15:37.084] [ci_solver] [info] DAV_NITER = 12, E0 = -6.323965e+00 Eh, DAVIDSON_DUR = 4.61096e-01 ms\n", + "CASCI energy is -229.419 Hartree, and the electron correlation energy is -0.153 Hartree\n" + ] + } + ], "source": [ "# Construct Hamiltonian in the active space and print its summary\n", "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", @@ -237,10 +302,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "ebba573c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total determinants in the CASCI wavefunction: 400\n", + "Plotting the top 10 determinants by weight.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9cf9edec687c4db7be9cd809ac440ee3", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import numpy as np\n", "from qdk.widgets import Histogram\n", @@ -266,13 +354,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "560f8554", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reference energy for top 2 determinants is -229.397913 Hartree\n" + ] + } + ], "source": [ "# Get top 2 determinants from the CASCI wavefunction to form a sparse wavefunction\n", - "top_configurations = wfn_cas.get_top_determinants(max_determinants=2)\n", + "top_configurations = wfn_cas.get_top_determinants(max_determinants=8)\n", "\n", "# Compute the reference energy of the sparse wavefunction\n", "pmc_calculator = create(\"projected_multi_configuration_calculator\")\n", @@ -282,230 +378,933 @@ ] }, { - "cell_type": "markdown", - "id": "e133afd5", + "cell_type": "code", + "execution_count": 7, + "id": "b3708467", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Determinant: 000111000111, Coefficient: 0.787612\n", + "Determinant: 001011001011, Coefficient: -0.580092\n", + "Determinant: 010110010110, Coefficient: -0.099398\n", + "Determinant: 100101100101, Coefficient: -0.104064\n", + "Determinant: 101001101001, Coefficient: 0.075494\n", + "Determinant: 011010011010, Coefficient: 0.074410\n", + "Determinant: 100011001101, Coefficient: -0.074884\n", + "Determinant: 001101100011, Coefficient: -0.074884\n" + ] + } + ], "source": [ - "### Loading the wavefunction using general state preparation methods\n", - "\n", - "One possibility for loading the multi-configuration wavefunction onto a quantum computer is to use general state preparation approaches such as the [isometry method](https://arxiv.org/abs/1501.06911), as offered in software such as [Qiskit](https://qiskit.org/documentation/stubs/qiskit.circuit.library.Isometry.html).\n", - "While this is a very powerful general-purpose approach, it can be resource intensive, requiring very deep circuits even for modest-sized wavefunctions due to its exponential scaling in the number of qubits.\n", - "This approach also requires numerous fine rotations—operations that can be challenging for near-term fault-tolerant quantum hardware.\n", - "This cell demonstrates how to use the isometry method to generate a quantum circuit for preparing the multi-configuration wavefunction on a quantum computer.\n", - "\n", - "**Note**: the generated circuits are so deep that you will need to adjust the \"zoom\" selection in the visualization window to see the detailed operations." + "for det, coeffs in zip(wfn_sparse.get_active_determinants(), wfn_sparse.get_coefficients()):\n", + " alpha_str, beta_str = det.to_binary_strings(6)\n", + " print(f\"Determinant: {alpha_str[::-1]}{beta_str[::-1]}, Coefficient: {coeffs:.6f}\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "a1bb218f", + "execution_count": 8, + "id": "9baaf705", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-04-02 17:15:40.744314] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", + "[2026-04-02 17:15:40.765453] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", + "[2026-04-02 17:15:40.774934] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", + "[2026-04-02 17:15:40.791613] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", + "[2026-04-02 17:15:40.800593] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d5fef22a0d824055914a48b7c10bfafd", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Logical EstimateCounts
0numQubits12
1tCount0
2rotationCount62
3rotationDepth62
4cczCount0
5ccixCount0
6measurementCount0
\n", + "
" + ], + "text/plain": [ + " Logical Estimate Counts\n", + "0 numQubits 12\n", + "1 tCount 0\n", + "2 rotationCount 62\n", + "3 rotationDepth 62\n", + "4 cczCount 0\n", + "5 ccixCount 0\n", + "6 measurementCount 0" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "import pandas as pd\n", - "from qdk.openqasm import estimate\n", "from qdk.widgets import Circuit\n", + "import qsharp\n", "\n", - "# Generate state preparation circuit for the sparse state using the regular isometry method (Qiskit)\n", - "state_prep = create(\"state_prep\", \"qiskit_regular_isometry\", transpile=False)\n", - "regular_isometry_circuit = state_prep.run(wfn_sparse)\n", + "# Generate state preparation circuit for the sparse state via sparse isometry (GF2 + X)\n", + "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "sparse_isometry_circuit = state_prep.run(wfn_sparse)\n", "\n", - "# Visualize the regular isometry circuit\n", - "display(Circuit(regular_isometry_circuit.get_qsharp_circuit()))\n", + "# Visualize the sparse isometry circuit\n", + "display(Circuit(sparse_isometry_circuit.get_qsharp_circuit(prune_classical_qubits=True)))\n", "\n", "# Print logical qubit counts estimated from the circuit\n", - "df = pd.DataFrame(estimate(regular_isometry_circuit.get_qasm()).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "df = pd.DataFrame(\n", + " qsharp.estimate(\n", + " sparse_isometry_circuit._qsharp_factory.program, None, *sparse_isometry_circuit._qsharp_factory.parameter.values()\n", + " ).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", "display(df)" ] }, { - "cell_type": "markdown", - "id": "f577f124", + "cell_type": "code", + "execution_count": null, + "id": "3d942859", "metadata": {}, + "outputs": [], "source": [ - "### Loading the wavefunction using optimized state preparation methods\n", + "from qdk_chemistry.data import (\n", + " Ansatz,\n", + " BasisSet,\n", + " CanonicalFourCenterHamiltonianContainer,\n", + " CasWavefunctionContainer,\n", + " Configuration,\n", + " Hamiltonian,\n", + " Orbitals,\n", + " OrbitalType,\n", + " Shell,\n", + " Structure,\n", + " Wavefunction,\n", + ")\n", + "\n", + "def create_test_basis_set(num_atomic_orbitals, name=\"test-basis\", structure=None):\n", + " \"\"\"Create a test basis set with the specified number of atomic orbitals.\n", + "\n", + " Args:\n", + " num_atomic_orbitals: Number of atomic orbitals to generate\n", + " name: Name for the basis set\n", + " structure: a structure to attach\n", + "\n", + " Returns:\n", + " qdk_chemistry.data.BasisSet: A valid basis set for testing\n", + "\n", + " \"\"\"\n", + " shells = []\n", + " atom_index = 0\n", + " functions_created = 0\n", "\n", - "As the cell above illustrates, the general isometry method for state preparation can be very resource intensive—requiring thousands of fine rotations for this benzene diradical example.\n", - "However, we can optimize this process by taking advantage of the sparse multi-configuration wavefunction structure, generating much more efficient quantum circuits for state preparation.\n", - "The cell below demonstrates how the `qdk-chemistry` library can be used for optimized wavefunction loading, producing a circuit that is orders of magnitude more efficient than the general isometry method.\n", + " # Create shells to reach the desired number of atomic orbitals\n", + " while functions_created < num_atomic_orbitals:\n", + " remaining = num_atomic_orbitals - functions_created\n", "\n", - "The underlying approach is based on a variation of the [sparse isometry method](https://quantum-journal.org/papers/q-2021-03-15-412/pdf/), with new updates specific to `qdk-chemistry` that avoid the use of multi-controlled gates (also challenging for near-term fault-tolerant quantum computers)." + " if remaining >= 3:\n", + " # Add a P shell (3 functions: Px, Py, Pz)\n", + " exps = np.array([1.0, 0.5])\n", + " coefs = np.array([0.6, 0.4])\n", + " shell = Shell(atom_index, OrbitalType.P, exps, coefs)\n", + " shells.append(shell)\n", + " functions_created += 3\n", + " elif remaining >= 1:\n", + " # Add S shells for remaining functions (1 function each)\n", + " for _ in range(remaining):\n", + " exps = np.array([1.0])\n", + " coefs = np.array([1.0])\n", + " shell = Shell(atom_index, OrbitalType.S, exps, coefs)\n", + " shells.append(shell)\n", + " functions_created += 1\n", + " if structure is not None:\n", + " return BasisSet(name, shells, structure)\n", + " return BasisSet(name, shells)\n", + "\n", + "\n", + "def create_test_orbitals(num_orbitals: int):\n", + " \"\"\"Helper: construct Orbitals immutably with identity coeffs and occupations.\n", + "\n", + " Occupations are set in restricted form as total occupancy per MO (0/1/2).\n", + " \"\"\"\n", + " coeffs = np.eye(num_orbitals)\n", + " basis_set = create_test_basis_set(num_orbitals)\n", + " return Orbitals(coeffs, None, None, basis_set)\n" ] }, { "cell_type": "code", - "execution_count": null, - "id": "9baaf705", + "execution_count": 1, + "id": "67663ad2", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n let rules = [\n {\n token: \"comment\",\n regex: /(\\/\\/).*/,\n beginWord: false,\n },\n {\n token: \"string\",\n regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n beginWord: false,\n },\n {\n token: \"keyword\",\n regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(not|and|or)\\b|(w/)`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(=)|(!)|(<)|(>)|(\\+)|(-)|(\\*)|(/)|(\\^)|(%)|(\\|)|(&&&)|(~~~)|(\\.\\.\\.)|(\\.\\.)|(\\?)`,\n beginWord: false,\n },\n {\n token: \"meta\",\n regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n beginWord: true,\n },\n {\n token: \"atom\",\n regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n beginWord: true,\n },\n ];\n let simpleRules = [];\n for (let rule of rules) {\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(rule.regex, \"g\"),\n sol: rule.beginWord,\n });\n if (rule.beginWord) {\n // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n sol: false,\n });\n }\n }\n\n // Register the mode defined above with CodeMirror\n window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n reg: [/^%%qsharp/],\n };\n\n // Force re-highlighting of all cells the first time this code runs\n for (const cell of window.Jupyter.notebook.get_cells()) {\n cell.auto_highlight();\n }\n });\n}\n", + "text/plain": [] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "ename": "TypeError", + "evalue": "__init__(): incompatible constructor arguments. The following argument types are supported:\n 1. qdk_chemistry._core.data.Orbitals(arg0: qdk_chemistry._core.data.Orbitals)\n 2. qdk_chemistry._core.data.Orbitals(coefficients: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n 3. qdk_chemistry._core.data.Orbitals(coefficients_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], coefficients_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, energies_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n\nInvoked with: array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]), None, None", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m 3\u001b[39m num_qubits = \u001b[32m26\u001b[39m\n\u001b[32m 4\u001b[39m seed = \u001b[32m42\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m wfn = \u001b[43mgenerate_random_wavefunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn_electrons\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnum_qubits\u001b[49m\u001b[43m/\u001b[49m\u001b[43m/\u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_orbitals\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnum_qubits\u001b[49m\u001b[43m/\u001b[49m\u001b[43m/\u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_dets\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnum_qubits\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mseed\u001b[49m\u001b[43m=\u001b[49m\u001b[43mseed\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/workspaces/qdk-chemistry/examples/random_wavefunction.py:422\u001b[39m, in \u001b[36mgenerate_random_wavefunction\u001b[39m\u001b[34m(n_electrons, n_orbitals, n_dets, seed, max_excitation_order, n_alpha, n_beta, include_hf)\u001b[39m\n\u001b[32m 419\u001b[39m coeffs = raw / np.linalg.norm(raw)\n\u001b[32m 421\u001b[39m \u001b[38;5;66;03m# Identity MO coefficients — sufficient for state-preparation benchmarks\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m422\u001b[39m orbitals = \u001b[43mOrbitals\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43meye\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn_orbitals\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 424\u001b[39m container = CasWavefunctionContainer(coeffs, dets, orbitals)\n\u001b[32m 425\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m Wavefunction(container)\n", + "\u001b[31mTypeError\u001b[39m: __init__(): incompatible constructor arguments. The following argument types are supported:\n 1. qdk_chemistry._core.data.Orbitals(arg0: qdk_chemistry._core.data.Orbitals)\n 2. qdk_chemistry._core.data.Orbitals(coefficients: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n 3. qdk_chemistry._core.data.Orbitals(coefficients_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], coefficients_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, energies_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n\nInvoked with: array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]), None, None" + ] + } + ], "source": [ - "import pandas as pd\n", - "from qdk.widgets import Circuit\n", - "import qsharp\n", + "from random_wavefunction import generate_random_wavefunction\n", "\n", - "# Generate state preparation circuit for the sparse state via sparse isometry (GF2 + X)\n", - "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", - "sparse_isometry_circuit = state_prep.run(wfn_sparse)\n", + "num_qubits = 26\n", + "seed = 42\n", + "wfn = generate_random_wavefunction(n_electrons=num_qubits//2, n_orbitals=num_qubits//2, n_dets=num_qubits, seed=seed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76edbb9c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-04-02 17:15:44.716994] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", + "[2026-04-02 17:15:44.727282] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", + "[2026-04-02 17:15:44.735311] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", + "[2026-04-02 17:15:44.744733] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", + "[2026-04-02 17:15:44.751399] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n", + "[2026-04-02 17:15:44.784098] [info] [sparse_isometry_binary_encoding] Binary encoding produced 20 operations (20 encoded) using 0 ancillae for 12-qubit system with 8 determinants\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2a5bb763df4b4c00ab1d18c2f29b3365", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Logical EstimateCounts
0numQubits14
1tCount7
2rotationCount7
3rotationDepth7
4cczCount7
5ccixCount0
6measurementCount6
\n", + "
" + ], + "text/plain": [ + " Logical Estimate Counts\n", + "0 numQubits 14\n", + "1 tCount 7\n", + "2 rotationCount 7\n", + "3 rotationDepth 7\n", + "4 cczCount 7\n", + "5 ccixCount 0\n", + "6 measurementCount 6" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from sparse_isometry_binary_encoding import SparseIsometryBinaryEncodingStatePreparation\n", "\n", - "# Visualize the sparse isometry circuit\n", - "display(Circuit(sparse_isometry_circuit.get_qsharp_circuit(prune_classical_qubits=True)))\n", + "sp_select = SparseIsometryBinaryEncodingStatePreparation()\n", + "sp_select.settings().update(\"use_measurement_and\", True)\n", + "select_circuit = sp_select.run(wfn_sparse)\n", + "display(Circuit(qsharp.circuit(select_circuit._qsharp_factory.program, *select_circuit._qsharp_factory.parameter.values(), generation_method=qsharp.CircuitGenerationMethod.Static)))\n", "\n", - "# Print logical qubit counts estimated from the circuit\n", "df = pd.DataFrame(\n", - " qsharp.estimate(\n", - " sparse_isometry_circuit._qsharp_factory.program, None, *sparse_isometry_circuit._qsharp_factory.parameter.values()\n", - " ).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + " qsharp.estimate(select_circuit._qsharp_factory.program, None, *select_circuit._qsharp_factory.parameter.values()).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", "display(df)" ] }, { - "cell_type": "markdown", - "id": "6635d626", + "cell_type": "code", + "execution_count": 13, + "id": "de2cacba", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-04-02 16:34:20.602273] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", + "[2026-04-02 16:34:20.619806] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", + "[2026-04-02 16:34:20.629731] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", + "[2026-04-02 16:34:20.637684] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", + "[2026-04-02 16:34:20.644554] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n", + "[2026-04-02 16:34:21.074222] [info] [sparse_isometry_binary_encoding] Binary encoding produced 20 operations (20 encoded) using 0 ancillae for 12-qubit system with 8 determinants\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a85114c3b21146aa8c34c5ee0cafc78f", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Logical EstimateCounts
0numQubits14
1tCount7
2rotationCount7
3rotationDepth7
4cczCount7
5ccixCount0
6measurementCount6
\n", + "
" + ], + "text/plain": [ + " Logical Estimate Counts\n", + "0 numQubits 14\n", + "1 tCount 7\n", + "2 rotationCount 7\n", + "3 rotationDepth 7\n", + "4 cczCount 7\n", + "5 ccixCount 0\n", + "6 measurementCount 6" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "Rather than requiring thousands of fine rotations, this optimized approach requires only a single fine rotation for the two-determinant benzene diradical wavefunction—demonstrating the power of chemistry-informed optimizations for quantum state preparation.\n", + "sp_select = SparseIsometryBinaryEncodingStatePreparation()\n", + "sp_select.settings().update(\"use_select_pui\", True)\n", + "sp_select.settings().update(\"use_measurement_and\", True)\n", + "select_circuit = sp_select.run(wfn_sparse)\n", + "display(Circuit(qsharp.circuit(select_circuit._qsharp_factory.program, *select_circuit._qsharp_factory.parameter.values(), generation_method=qsharp.CircuitGenerationMethod.Static)))\n", "\n", - "Close inspection of the generated circuit shows that it has also reduced our qubit count: several of the qubits have been converted to classical bits, which can be post-processed after measurement.\n", - "We will revisit these classical bits in the next section on energy measurement." + "df = pd.DataFrame(\n", + " qsharp.estimate(select_circuit._qsharp_factory.program, None, *select_circuit._qsharp_factory.parameter.values()).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "display(df)" ] }, { - "cell_type": "markdown", - "id": "4588419b", + "cell_type": "code", + "execution_count": 15, + "id": "6c38dd5e", "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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AACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEAAAAAAFAoAhkAAAAAAACFIpABAAAAAABQKAIZAAAAAAAAhSKQAQAAAAAAUCgCGQAAAAAAAIUikAEA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AAAAAIAWUFnqAlrblClTcuSRR2bhwoXp0qVLhg8fnvnz5+eSSy7JrFmzsnjx4iTJqFGjSlsoAAAAAAAALaJQM8iqqqpyzDHHZOHChTnrrLOyYMGCPPLII1m4cGEuvPDC3HTTTZk0aVLKysoyYsSIUpcLAAAAAABACyhUQHbGGWdk7ty5Of3003PxxRenW7du67dNmDAhI0eOTE1NTQYOHJju3buXsFIAAAAAAABaSmECsunTp+eaa65J7969c/75529yn9GjRydJRo4cuf61dYHamDFj0qFDh5SVlW22j2effTbHHntsunXrlp49e+ajH/1oXnrppeY9EQAAAAAAALZIYQKyq6++OnV1dTnppJPStWvXTe7TqVOnJBsGZDNnzsx1112XPn36ZN99991s+0uXLs24ceMyd+7cXH311bn88stz77335uijj05dXV3zngwAAAAAAABNVlnqAlrLxIkTkyTjxo3b7D5z585NsmFAduCBB2bBggVJkm984xu5//77N3ns5Zdfnnnz5uWee+7JLrvskiQZMGBA3vGOd+T666/Pe97znuY4DQAAAAAAALZQYQKy5557Lkmy6667bnJ7TU3N+vDrtQFZeXnDJtndeOONOeCAA9aHY0kyduzYDBo0KDfccEOTA7LBgwc3uAYAAICWsKDHfyfl22XBwgUZMGBAqctpdUU//8R7sNOlz6WsvCK1dbUZMGDT1xWAbVvRPweLfv6J98BYSFvy2lX7DjjggDz66KNNaqcwAVl1dXWSZMWKFZvcfs0116SqqirdunXLbrvt1uj2p02blhNOOGGj1/fcc89Mmzat0e2ts272GgAAQMl0q03Kk7ra2sybN6/U1bS+op9/Uvj3YKf6+rVf1NcX8vyBFP5zsPDnnxT+PTAW0lYtWrSoyccWJiDr06dPlixZkkceeSRjx47dYNuCBQty9tlnJ0lGjBiRsrKyRre/ZMmS9OjRY6PXe/XqlaeeeqpJNSdJ3759zSADAABKakFFReqSlFdUpG///qUup9UV/fwT70HWXScoK0v/Ip4/UPjPwaKff+I9MBbSltTV1a2fXLTTTjs1uZ3CBGTjx4/P9OnTc+GFF+bQQw/NkCFDkiSTJk3KySefnKqqqiTJqFGjSljlxp5++ul06dKl1GUAAAAFNmD81Zn3wvL07dM3c5+YW+pyWl3Rzz/xHoy5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QLP6y53PZcqTDV8abEu9vHR1/ve3U1utv+b2xNOL89nvPJBFL61Ikix6aUXOuOAfLfK8GtqeuvrkwReSCZOSunWvJfnl08nijW+k3ubcXfVC2t/wh/xg1pOb3af9DX/Iex66txWrahnPPvtsvvzlL+fqq6/eIBzr3Llz9txzzxx11FH50Ic+lA996EM56qijsueee6Zz587r91u8eHGuvvrqfPnLX86zzz67UTjWsWPHnHPOORk6dGirn1truP322/OhD31os7Pt2ppvXTal0TeKbIkb7n4+k554sdX6a041NXX5653P5ejTb1s/Fr6weGV++Nsn8vKrBfggJNU1ybWzk1Pv23As/Pv8pKbuDQ7cBhRpHKyvr88dd9yRs88+O/fcc88G4dgOO+yQMWPG5Pjjj8+HPvShnHjiiTnkkEMycODADWZPP/XUU7ngggvy05/+NNXV1RuFY7vvvnu+/OUvbzB+tgWFm0E2ZcqUHHnkkVm4cGG6dOmS4cOHZ/78+bnkkksya9as9b8IvTbpBAAA2Bb85Jrprd7nFX+ekfM+87Z06rj1/PNz8SurctI5d+Vv98/d4PWa2vr86P+m5Uf/Ny3HHLRLfvOdA9Oje4cSVUlLevqV5JyHk+c2MeHnx9OTy55MTto9+eywpLys9euj+dx222359a9/ndra2iRrl4x629velsMOOyx77713yss3fW95XV1dHn/88fz973/P5MmTU19fn+effz5f/vKX069fv/XPHFsXjr3lLW9ptXNqirvuuit33313zj333PWv1dXV5ZZbbskdd9yRF198Md26dcvYsWNzwgknpGPHjuv3Gz16dK688so8/PDDGT9+fCnKb7D5L1TnL3c+1+r9/vQP07PvXju0er9b4t6HF+akL92VOQurN3h9TU1dPve9h/LlSx7Ouae9NWd/fO+Ulfkg3BZd+2zyo+lrQ7LXO2dy0rtDcu5bk7E7tn5tNJ9Vq1blkksuycMPP7z+tS5duuTggw/O+PHj07dv380eu2LFitx77725/fbbM2fOnCTJ3XffnUcffTS1tbWprl77+bEuHOvSpe2tKlGoGWRVVVU55phjsnDhwpx11llZsGBBHnnkkSxcuDAXXnhhbrrppkyaNCllZWUZMWJEqcsFAABoNk8++3LumrSg1ftd/MqqXHv7s63eb1MtfmVV3vmxGzcKx17vhrufz8Gn3pxXlq5upcpoLU++nHzy/k2HY+vU1Ce/npl8c0pS33oTUWhmN954Y6688sr14dguu+yS7373uzn77LMzcuTIzYZjSVJeXp6RI0fmrLPOygUXXJCBAwcmWXsX/tYWjm3Ob37zm1x11VXp379/Pvaxj2W//fbL3/72t1x00UWpq/v31JFevXpl0KBBmTx5cgmrbZhf/GlGamtb/y/t1bc8kyVb0Yyrvz84L+M/fctG4dhrLV9Zky/+76R86Ydt/+dO4/3y6eSCxzcdjq1TtSo586Hkrtb/9ZJmsmrVqlx44YUbhGMHH3xwLrnkkpx88slvGI4lSadOnXLYYYfle9/7Xj796U+nU6dOSZJXX311qwjHkoIFZGeccUbmzp2b008/PRdffHG6deu2ftuECRMycuTI1NTUZODAgenevXsJKwUAAGhedzw0v4R9bz1XTj7+tXsybdbLDdr3sacW51Pn3deyBdGqVtYmn3vojS8IvtaNc5I/zG7Rkmgh99xzT37729+u//6oo47Kd7/73ey2226NbmvXXXfN17/+9eyww4YzhA4//PCtNhybM2dObr311owZMyZnnXVW3vWud+WjH/1oTj755EydOjUPPPDABvvvs88+mTp1alauXFmiihumVGPhylW1+cdjL5Sk78Z64aUVed/n78jqNQ1bP+/CK/+Va2/bem6E4c09+MLa2dINUVeffOXhZP7ylq2J5ldXV5dLLrkk06ZNS7I27JowYUJOO+20RodZZWVlOeSQQ/L5z39+g5tLysvL86lPfarNhmNJgQKy6dOn55prrknv3r1z/vnnb3Kf0aNHJ0lGjhy5/rV1gdqYMWPSoUOHzU4Zvvbaa3P88cdn1113TefOnfOWt7wlX/nKVzyAFQAAaBMenlZVyL4b4+nnXsn1dz3fqGOu+/vsPDd/aQtVRGu7fd7aO+Ib4+pZay8QbquW19amatWqTf63tXrhhRdy5ZVXrv/+hBNOyMknn5zKyqYtBbt69er88Ic/zIsvbvicqb/97W9ZtGjRFtVaKg888EDq6+tz5JFHbvD6IYcckg4dOuS++za8OWCfffbJmjVrMmXKlFassnHq6urz6JMvlaz/rWUsvOLPM7K0es2b7/ga//PbJ1qoGkrh/55p3P6r6pI/zW6RUtqEbXEcTJI77rhj/cyxTp065atf/Wre9ra3Nbm9OXPm5Ec/+tEGM4zr6urym9/8ZoPX2pqtZxH4LXT11Venrq4uJ510Urp27brJfdZNAXxtQDZz5sxcd9112XfffdO+ffvcf//9mzz24osvXj8Vf8CAAZkyZUrOO++83H333bnnnnvecFo+AABAS3tkeukuCk575uWsWFnT5p9D9rM/bv4h7JtTV1efy699Kt85Y58WqIjW9sfZjT9m7vLkwReTd2yjz2D55lNT882nppa6jGZTX1+fyy+/fP1Mp4MPPjjve9/7mtze6tWr8/3vfz+PPfZYkrXLKu655555+OGHs2rVqlx22WX56le/utVdF5o1a1bKysqyxx57bPB6+/bts+uuu2bWrFkbvL7zzjunT58+mTx5cvbbb7/WLLXBZj7/aqODn+ZUynG4oWpr6/KzPzb+eaUPTHkhU558KaPesn0LVEVrmlud/KMJkx3/8nzyqaFJh4rmr6nUtrVxMFl7o8jvfve79d+feeaZ2X333Zvc3pw5c/LNb34zS5euvWlst912yyuvvJLFixdn2rRp+fvf/57DDjtsi+tuCW37XyfNaOLEiUmScePGbXafuXPXrjH/2oDswAMPzIIFa5cD+cY3vrHZgOyGG27YYCr9QQcdlB122CEnnXRS7rvvvhx44IFNqnvw4MFb3S9RAABA27OgxxeS8m6b3Dbp6mPTp3fnzR7bp3en9f+fc/sHN7vfwqrl2fdD12/0el1dfQYNHpGK+lcbWXXreqH7J5PKnRt93MU//XN+/b33NH9BbciCHv+dlG+XBQsXZMCAAaUup2WUV6bPpbObdOgnvvY/WXbT95u3nibqVF6eaaPGNlt7n9xlUI7vt+m/F0c+eHez9DFkyJCsaKa7y9u3b7/ZlYOSZPLkyXniibWzXbbffvt89KMf3exqQW9mU+HYOeeck1133TVnn312qqqqMm3atEyaNClvf/vbN9vOkCFDsnp18zzP8M3Ov6GWLFmS7t27p127dhtt69WrV2bMmJGampoNZt2NHj06d911V2pra1NR0fCr5M15/m9kVeWuSfdPbHLbm42DyZaPhTffem8GDPhYwwsugZry7lnU46wmHTvuqFPTZdW2/TyyIoyFHfd5T3p84tJGH/fy6mTofoekZsGMFqiqcYo+DiZvPhb8/ve/X3+jyLve9a6MGjWqyX29Phxb98yxZ555Jt/5zneSrJ28dOCBB6Zjx46bbaexY8FrZ6UdcMABefTRR5tUf2ECsueeey7J2nWhN6WmpmZ9+PXagKyh4dTr15lO1k4vT7L+4axNsS6cAwAA2CLdyza7yH6f3p0zYKc3fzZAZUV5g/bblIWLXkzWtPHlpTqVNelfyavXlG3Rv/u2Ct1qk/KkrrZ2mz3X8s7d06eJx1avqWsz70vniopkVPO1t0fXrnnXDjs1X4ObMH/+/CyvrW2Wtjp06PCG22+77bb1X3/84x9P585vHIpszubCsXXPHPvEJz6R733ve+v7fKOAbP78+VnVTEt1vdn5N9SqVas2u+TkutDs9fvss88+uemmmzJ9+vTstddeDe6rOc//DXXpnnTf9KaGjoNJ08fC1W3oc2KzOtQmPZp26MuvrsjLVW38/LZUAcbC3sNXNfWPQF58tTrL28D7UvRxMHnjseDll1/OQw89lCTp1q1bTjrppCb3s7lwrEuXLtl7771z4IEH5p577smKFSty3333Zfz48Ztta0vGgi1ZzrgwAVl1dXWSZMWKFZvcfs0116SqqirdunVr0gNZN+XOO+9MkgwbNqzJbfTt29cMMgAAYIstKK/L5u5LXVj1xk9W79O7UyorylNTW5eFVZv+N9WbtdNnp96pqG+eC7ct5cXK2jRlDkOHdnXp3b9/s9fTliyoqEhdkvKKivTdVs+1rOn/9u5cmfRvI+9Lp63wGkK/fv2adQbZ5syfPz+PP/54kmSnnXZq8rNW3iwcS5JRo0alb9++WbBgQaZOnZp58+Zt9s9Iv379mnUGWXPo0KFDXn1107N+16xZs36f11p3/aqxz5ppzvN/I6sqe2Rzt2m82TiYbPlY2L5deXZoI58Tm1Nb1i0Lk6S+PmnkzMoe3TumS4e2fX5bqghjYcdOTY8LenfrlNo28L4UfRxM3ngsuPPOO1P7/8O4Qw45pMk3irxROLbOu9/97txzzz1J1t4s8q53vWuzs7YbOxbU1dWtn1y0005NDzALE5D16dMnS5YsySOPPJKxYzecYrlgwYKcffbZSZIRI0Y0eWr9a82bNy9f+9rXcsQRR2zRFMWnn356gz9UAAAATTHmw3/NpCc2fWlwU0tBvdac2z+YATt1ycKqFdn50N83uu/27crz3DNPpH27tv1givN++ki+8dPGL89y3tkn5ouf+G4LVNR2DBh/dea9sDx9+/TN3CfmlrqcFvOZB5J/NmGi4x8vOCsjLm/asmTNrX7lytR84JRSl9EoM2bMSNkbLLvUGDU1Nbnuuus2ue3hhx9e//X48eObdENyQ8KxZG1YNH78+Fx11VVJkkmTJm02IJsxY8ZmZ2s11hudf2P07Nkzc+fOzZo1azZaZnHx4sXp1q3bRjVPnjw5nTt3zvDhwxvVV3Oe/xuZPW9pdjvyD5vc9mbjYLLlY+F7jj4o11x0XqOPa0319fXZ631/yrRZLzf62Ml3XZXdd97MFL1tRBHGwqqVyVG3J7X1jTuuT6fkoUl3pWLLL6tvsaKPg8kbjwWTJk1a//W73vWuJrXfkHAsSQYOHJjBgwfn6aefzvPPP59FixalT59Nz9dv7FhQXV2drl27Jknuu+++Jp1HstkFNrY966bvXXjhhZkx499roU6aNCnjxo1LVdXa34C3JMxaZ9myZTnuuOPSvn37XHnllVvcHgAAwJYaPax3yfree3CvNh+OJcmnjh+aikZe2WnfrjyfeM+QFqqI1vb+gY0/Zkj3ZO+ezV4KLWDWrFnrv37t4zUaqqHh2Kb6eOaZZxrdXyntvvvuqa+vz8yZMzd4ffXq1XnuuecyaNCgjY6ZPHlyRo0a1SphV1Ps2q9rem1XupnMpRyHG6qsrCyfObHxK2Edsf+AbT4cK4reHZND+jb+uOMHpk2EY7yxmpqaPP/880nWztjacccdG91GQ8OxdUaMGLH+62effbYJVbeswgRkEyZMyPbbb585c+Zkzz33zN57753BgwdnzJgxGTRoUA455JAkTfsF6bVWrFiRY445Js8++2xuu+229O3bhE8UAACAZjZ6eOkuzI0evn3J+m6Mfjt2yceOHdyoYz51/NDs0KtTC1VEazuwTzKoW+OO+djgRq9ERomsuzDXvn37Ri+J2dhwLFl78XHdMoRt8aLgGxk7dmzKyspyyy23bPD6xIkTs2rVqhxwwAEbvD5v3rwsWLAg++yzT2uW2ShlZWUlHY9KOQ43xslH75H+OzZ8ybXy8rJM+PjeLVgRre0juzcu7OrRPjlul5arh+YzZ86c1NTUJMkmb3RoyPGNCcde309bvFmkMAHZgAEDcu+99+aoo45Kx44dM3v27PTq1SuXXXZZbrrppvWzyrYkIFuzZk3e//73Z/LkybnlllsaPaUcAACgpRz2jv4pLy/NVfwjDxhQkn6b4tIvj80hYxp2o+MR+w/ID85+ewtXRGuqLE/+9+3JTg3MPD89NDms9I9boQHq6+vzwgsvJFkbXFVUNHxWa1PCsWTtMovrgrgXX3yx0c/mKqVddtklhx12WP75z3/m+9//fiZOnJirrroqV111VYYNG5b9999/g/0nT56cysrKZlmZqSUdecDOJel3u27ts9+IHUrSd2N179o+N/34sAbPtvvxl8dm3Jh+LVwVrWnPnsm5oxoWHHSuTP5nTNKrbT9mlv9v0aJF67/eeefGfR42JRx7fT/rxuG2pG3OeW4hw4YNy4033rjR68uWLcvs2bNTXl6evfbaq0lt19XV5aSTTsodd9yRm2++OWPGjNnScgEAAJrNLn275piDds5f73y+VfvduU+XHH3g1nNbcccOlbn5J4fnrIsfyhV/npGVq2o32qdzx8p8+v1Dc+Hn990qlo6kcfp1Tn55QPKdx5L7N3MdZ/sOyWlvSd67a+vW1poO6r1jVh/zgTfc5822tyX19fXZZ599snr16kav9vOTn/yk0eHYOkOHDk23bt3Svn371NXVNem5Z61h4MCBG712yimnZIcddsgdd9yRRx99NN26dcvhhx+eD3zgAxudx+TJk7Pnnnumc+eGzzwqhY8dNzhf+dHkrFi58Wd7S/r4cYPTpXO7N9+xjRg5dPv846pj8qnz7ss9Dy/c5D4D+3XN9/57TE44bLdWro7W8O6dk+7tkx88kTxfvel99uqZfGVEMni71q2ttWxr42CSbLfddhk9enSjx8KXX365SeFYknTu3Dl77rln2rdvn912a3ufF4UKyDZn6tSpqa+vz5AhQzY5kF977bVJkmnTpm3w/cCBA9dPHf/sZz+bP/7xjznnnHPSuXPnPPjgg+uP33333bPDDlvHXSIAAMC26zMnDmv1gOzT7x+aysq2eUF4czq0r8ilX35HvvnZ0fnVX2fkK5c8nJWra9OxfUUu+Nw+OeXYwenR3a3S27IdOyU/3C+Zsyz58/PJM0uT1bXJdu2Td/VLDu6zdrYZW4/y8vKcddZZTTr2yCOPzGOPPZb6+vpGhWPJ2pBpazBw4MCNQrLy8vIcffTROfroo9/w2JdffjkzZ87Mxz/+8RassHn07N4hHzpy91z55xmt2u9pJzT8z0xbMWTgdrn7l0fl8RmL8/M/PZXLr30yq1bXpVOHivzx+4fkiP0HpKLCB+G27ICdknfsmPzzxeSWuUnVqrVLL/brnLxnl+QtPUpdIY01bNiwDBvW+OcMbrfddjnssMNy3XXXNSocS5KuXbvma1/7WqP7bC0CsiSPP/54ks0vr3jCCSds8vtTTjklv/rVr5Jk/ZrMF1xwQS644IIN9v/lL3+Zj33sY81YMQAAQOMdOrZ/xu/XL39/cH6r9Ddgpy75rw/t2Sp9tYRe23XIf3907/zgN09k3gvLs32PDjnzI01bdYSt085dkzM8PaHwhg4dmnPOOSf19fWNCseKYvLkyUnSpp8/9lpf+/So/OHWZ7Ns+ZpW6e+T7xuSobv1aJW+WsLeQ3rlknPG5k9/n515LyxPr+065KitaGY4W6a8LNlvx7X/UVxlZWV5//vfn549e2bs2LENDse2BgKyvHlAVl9f/6ZtzJ49uzlLAgAAaHZlZWX5xTcOyF7v+3OrXBj8xTcOyHbd2rd4PwAtbejQoaUuoc0aN25cDjrooLRrt3UsITiwf7dc9N/75j+//UCL97Vzny75/hc8qxLY+pWVlWX8+PGlLqPZmQebNw/IAAAAthW79uuWS87Zr1HHLKxanrmLqrOwanmDjznthLfk8P0HNLY8ALYyFRUVW004ts5/nPCWHHlA48aoxo6FFRVlueK8d6Z7VzeKALRVZpAlmThxYqlLAAAAaDUff8+QzF1Una//+JEG7b/vh65vVPvvOWTX/OhLY5tSGgC0uLKysvzh4kNy6KdvyYP/erFBxzRmLCwrS674xjtz6Nj+TS0RgFZgBhkAAEABfe0/3poLPtf8z4v58Lt3zx8uOiSVlf65CUDb1bVzu9z6syMybt++zdpuZWVZfvvdg3PKcYObtV0Amp9/sQAAABTUFz8xMhN/cWQG9uu6xW1179ouV5z3zvz2/IPSrp1/agLQ9nXv2j63XXZEvnvGPmnfDGPXqLf0yuSrj8uHj9q9GaoDoKX5VwsAAECBjRvTL4//6X35/Ml7pkunxq/CX1FRlg8cvlue+NP78on3DklZWVkLVAkALaOysjxf+uTIPHLNe3LYO5q2JOL2PTrk26ePzj9/d1xGDt2+mSsEoKV4BhkAAEDBde3cLj84e7984z/flqtunJkr/zwjj81YnNra+s0es8cu3fPhI3fPp44fmgF9urRitQDQ/Pbco2du/dkRmTH7lfzsj9Pzx9tmZ+6i6s3u375decbstUM+/f6hOeGw3dKxg8usAFsbn9wAAAAkWbvU1Gc/ODyf/eDwrFhZk3/NWJx/zVicV6vXpK6uPp07VWbYbj3ytmHbp0f3DqUuFwCa3ZCB2+UHZ++XH5y9X154aUUenlaVmXNezcpVtamsLE+Pbu0zamiv7LlHz7RvV1HqcgHYAgIyAAAANtKpY2XePmLHvH3EjqUuBQBKYsftO+XId+5c6jIAaCGeQQYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAqS10AAAAAAM2kQ4dU/uHXpa6icTp0aLamKioqcvzxxzdbexdddk2WVlenW5cuOfs/Ttzo++ZQUVHRLO2sa6s5z781NOf5AxR9HEyMhY0hIAMAAADYRpSVlSUdO5a6jJIpKytLZWXzXe6qT1JXv/b/lZWVG33f1jT3+QNsbYo+DibGwsawxCIAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAqS10AAAAAQHOor69PVq0qdRmN06FDysrKmq25+vr61NbWNlt7raGioqJZ34Mi8/MHij4W+hykMQRkAAAAwLZh1arUfOCUUlfRKJV/+HXSsWOztVdbW5vrrruu2dprDccff3wqK12iag5+/kDRx0KfgzSGJRYBAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAA2Y2HV8qxeU5ckWb2mLoteWlHiigCg9dTW1uXp517J6jW1SZI1NXVZvqKmxFUBNI/KUhcAAAAAbcXLr67KVTfOzG0PzMvD01/KgheXr9/24pKV6TPu/9J/x84ZPbx3DntH/5x89B7p3rV9CSsGgOY1/ZmXc+WfZ+Qf/3ohU558KdWvCcReWLwy3cb+JsMH9cg+e/bOB48YlEPH9k95eVkJKwZoGgEZAAAAhffs3KU5/4rH8rubZmX5yje+M37eC8sz74Xnc/1dz+eL/zMpJx+9R770yZHZpW/XVqoWAJrfLffOyUW/ejx3TlrwhvvV1dXniZlL8sTMJfnVX5/O7jt3y2c/ODyf/eCwtG9X0UrVAmw5SywCAABQWHV19fnx76dl7+P/lJ9f99SbhmOvV72iJj/745PZ631/ys+vfTL19fUtVCkAtIzFr6zKR750V9792dveNBzblFlzlua/L3oo+37o+jw6vaoFKgRoGQIyAAAACumVpatz+Gl/y+nf/ccGy0c1xdLqNfn0N+/PUZ+9LUurVzdThQDQsv7x2KIMf891+d1Ns7a4rX/NWJwxJ12f/7nqiWaoDKDlCcgAAAAonCWvrsq4U2/O3x+c36zt3nLf3Bz66b/l1WVCMgDatrsmLcj4T/0ti15a0Wxt1tTU578veijf+MkjzdYmQEsRkAEAAFAoq1bX5qjP3pZHn3ypRdp/6PEXc+wZt2fNmroWaR+2JqtXr8706dNLXQbwOo9Or8ox/3V7o5cWbqjzfvZofvhbM8kgSebMmZPFixeXugw2obLUBQAAAEBrOu+nj+Yfj73Q4P0nXX1s+vTunIVVy7Pvh65v0DF3T16YC658LF/7j7c2tUwomZdeeilTpkzJs88+m2eeeSYvvfRS1qxZk4qKinTv3j0DBw7MoEGDMnz48Oy2226bbWf16tX5/ve/nyeeeCJnnnlmxowZ04pnAWzOylU1+ciX786y5WsafExTxsKzfzAp48b0y4ghvZpaKpREfX19Zs6cmenTp+eZZ57Jc889l2XLlqW2tjbt2rXLjjvumN122y2DBg3KqFGj0qNHj822NWfOnHzrW99K586d8/Wvfz29evn70JYIyAAAACiMSU+8mAt/+a9GHdOnd+cM2KlLo/v61mVTcty4XV0YZKtQX1+fJ554IrfddlsmT56c+vr6Te63dOnSzJs3L/fff3+SZPfdd89hhx2WsWPHpn379uv3WxeOPfbYY0mSyy67LHvttVc6d+7c8icDvKHzfvZops16uVHHNGUsXFNTl4999Z489Ltj066dhcxo+1auXJl77703t99+e55//vnN7vfKK6/k6aefTpJUVFRkv/32y2GHHZahQ4dusN+6cOzVV1/Nq6++mt/+9rc544wzWvQcaJxCfjJVVVVlwoQJ2WOPPdKxY8fsvPPOOfPMM1NdXZ1TTz01ZWVlufTSS0tdJgAAAM3s9O/+I3V1m77w39zW1NTlv87/R6v01VKqlqzMz699Mq9Wr51lsGJlTVavqS1xVTS3xYsX53vf+16+853vZNKkSRuEY2VlZendu3f69euXHXfcMZWVG95rPWvWrPz0pz/NOeeckxkzZiTZOBzr2LFjzj77bOHYNur222/Phz70oSxZsqTUpdAAs+a8mu/98vFW6+/RJ1/K5dc92Wr9tYQpT76U/7nqifVjoXFw2/TYY4/lrLPOyhVXXLFRONa+ffvstNNO6dev30YzwGpra3P//ffn3HPPzSWXXJJXX301yYbhWLL2hpJTTz21dU6GBivcDLIpU6bkyCOPzMKFC9OlS5cMHz488+fPzyWXXJJZs2atXwt01KhRpS0UAACAZjXpiRfzzydebNU+73l4YR6fsTh7b2WzyJ5+7pV867Ip+cNtz2bV6n9fCFz86ursfOjv88n3Dc2XPjkyXTu3K2GVLefuqhdy6D/uygXDR+S/d3/LJvdpf8Mf8u4d++Yvb39nK1fXvO67775ceeWVWb58+frXevbsmXHjxmXvvffOwIED06lTp/XbampqMmfOnDz11FO56667Mnv27CTJ/Pnzc+655+bII4/M3Llz869/rZ2p2bFjx5xzzjl5y1s2/T7SNtx11125++67c+65565/ra6uLrfcckvuuOOOvPjii+nWrVvGjh2bE044IR07dly/3+jRo3PllVfm4Ycfzvjx40tRPo3wsz882Wo3iqzz499Pz2dOHJaysrJW7XdL/fXO5/K9X/4rD0zZcFnmF5esyr4f+mu+cMreOfGIQSWqruUVZSxcvXp1fv3rX+eOO+7Y4PXBgwfnoIMOypAhQ9K/f/9UVFSs31ZdXZ1nn302jz32WO66664sXbo0SfLAAw9k6tSpee9735s//elPG4RjX/7yl9OlS+NXJKBlFSogq6qqyjHHHJOFCxfmrLPOyrnnnptu3bolSb73ve/li1/8YiorK1NWVpYRI0aUuFoAAACa049/P70k/f7kmun56df2L0nfTfHgYy/kqNNvy+JXVm1y+wuLV+a7v3gstz4wL7f+7PBs36PjJvej7bvxxhvz29/+dv33PXv2zMknn5wxY8ZsNFNsncrKyuy2227Zbbfdcvjhh2fGjBm56qqrMnPmzNTX1+fmm29ev69wbOv2m9/8Jn/729+y77775qijjsq8efPyt7/9LbNnz85XvvKVlJevXZiqV69eGTRoUCZPniwga+NWrKzJlX+Z0er9Tn/m5dw1aUHGjenX6n031Xcun5KvXvrwZrdPnlqVD064M49Mr8oFn9t3qwv/WGvlypW56KKLMnXq1PWv7bXXXjnppJPe8BmbXbp0yV577ZW99torJ5xwQu6777783//9X5YtW5ZXXnklv/rVr9bvKxxr2wq1xOIZZ5yRuXPn5vTTT8/FF1+8PhxLkgkTJmTkyJGpqanJwIED07179xJWCgAAQHOqra3LdX+fXZK+/3Dbs5t9nlNb88zcV98wHHuth6dV5dgzbs+aNXWtUBnN7bbbbtsgHDvggANy0UUX5R3veMdmw7HXKysry9ChQ3PeeeflxBNP3GBbeXl5JkyYIBzbSs2ZMye33nprxowZk7POOivvete78tGPfjQnn3xypk6dmgceeGCD/ffZZ59MnTo1K1euLFHFNMTEf85v0Od7S/jDrc+WpN+m+Pm1T75hOPZa3/vl4/nBb55o4YpoCWvWrMn3v//99eFYhw4d8slPfjJf+cpX3jAce7327dvnkEMOycUXX5zhw4dvsG2nnXYSjrVxhQnIpk+fnmuuuSa9e/fO+eefv8l9Ro8enSQZOXLk+tfWBWpjxoxJhw4dNns3wL333pvx48enb9++6dChQwYMGJATTzwx06eX5g5FAAAA/m3Gc69m2fI1Jel78SurMnvespL03VgXXPGvRl08fWDKC/nrnc+1YEW0hJkzZ+aXv/zl+u9POOGEfPazn03Xrl2b1F5tbW2efHLDZwzV1dXl2We3ngvibOiBBx5IfX19jjzyyA1eP+SQQ9KhQ4fcd999G7y+zz77ZM2aNZkyZUorVkljTZ5aVbq+p5Wu78ZYtbo2X/lRw8Kxdc772aNZWr26hSqipfzxj3/M44+vfR5f586d89WvfjXjx49v8mzApUuXZu7cuRu89tJLL+WVV17Z4lppOYUJyK6++urU1dXlpJNO2uwvfOvW1H5tQDZz5sxcd9116dOnT/bdd9/Ntr9kyZLsvffeueSSS3LbbbflwgsvzNSpUzN27NiN/mIAAADQuh4u8YW5UvffEK8sXZ3f3TSr0cf95Jpt98bQ5bW1qVq1apP/ba1Wr16dn/70p+tnNR5zzDE5/vjjm3xBcPXq1fn+97+fxx57LEnSrt2/n0v3+9//PvPnz9/yoml1s2bNSllZWfbYY48NXm/fvn123XXXzJq14WfFzjvvnD59+mTy5MmtWSaN9PC0l0rW979mLM7qNbVvvmOJ/envs/PiksbNhFxavaZJ4+fWYlscC59++unccMMNSdYuHfzFL34xgwcPbnJ7c+bMybe+9a31zxxbt3JdTU1Nfvazn6Wuzmz7tqowzyCbOHFikmTcuHGb3WddkPXagOzAAw/MggULkiTf+MY3cv/992/y2GOPPTbHHnvsBq/tu+++GTp0aK677rqceeaZW1Q/AAAATff0c6+WtP8Zz7X9u4dvumdOlq+safRxd05akBdeWpEdt+/UAlWV1jefmppvPjX1zXfcivz1r3/NvHnzkiSDBg3KBz/4wSa39fpwbN0zxx566KHccsstWbNmTX7+85/n3HPPbZbaaT1LlixJ9+7dNwg81+nVq1dmzJiRmpqaDZbjHD16dO66667U1tamoqKiNculgZ5+vnRj0eo1dXl+QXX22KVtP9bmmlufaeJxz+a0Dwxr5mrahm1tLKyrq8tll122/kaR448/PkOHDm1ye68Px3bfffecddZZOe+887Jo0aLMmDEjt99+ew4//PBmqZ/mVZiA7Lnn1i75sOuuu25ye01Nzfrw67UB2boHjjbF9ttvnyQNXrt7UwYPHrxFNQAAAJC80umwpNP+m9w26epj06d3580e26d3p/X/n3P7G4cJC6uWZ98PXb/R6xd87/v5yXl3NqLi1resw35JlyPffMdN2Put70i7uhebuaLG61RenmmjxjZbe5/cZVCO77fzJrcd+eDdzdLHkCFDsqIZ7yxv3779Zh8tsXr16tx2221JkoqKivznf/5nk4OMzYVjb3nLW7LbbrvlkUceyaJFizJ9+vTMmjUru++++2bbGjJkSFavbpvLk733459Ll67ds2DhggwYMGCj79uaN/r5N8aqVas2ez1rXWj2+n322Wef3HTTTZk+fXr22muvBvfVln/+25qF230+qeixyW2tMRbu/86D0672hYYXXAIvdjs1abdLo4+77x9TMmDAp1ugosYr+lj4Zp+Djz322PqJMoMGDdpo0ktjbCocW/fMsdNOOy3nnXdekuTmm2/OoYceutnr/G39c7AtjoWvnZV3wAEH5NFHH21SO4UJyKqrq5MkK1as2OT2a665JlVVVenWrVujHsL3erW1tamrq8tzzz2XL33pS+nTp08+8IEPNLm9dbPXAAAA2AI7vZpsZoJTn96dM2CnN394emVFeYP225Slr76SpS/Ma9KxrabXS0kTnyH/wqL5yerSX/TsXFGRjGq+9vbo2jXv2mGn5mtwE+bPn5/ltc237FiHDh02u+3BBx/M0qVLkyRjx47Nzjtv+oLnm3mjcGxdDccdd1wuv/zyJMntt9/+hgHZ/Pnzs6qNLtVV9/9/NnW1tZk3b95G37c1b/Tzb2w76y74vt6aNWs22de6C7+NXUqsLf/8tzld1ySbycRbYyx8YdHCZFUbv9a524pk44mTb6pmzco285lQ9LHwzT4Hb7/99vVfv+9972vyjSJvFI4lybBhw7L33nvn8ccfz6JFi/L4449vMDHntdr652BbHwsXLVrU5GMLE5D16dMnS5YsySOPPJKxYzdM0BcsWJCzzz47STJixIgmr7udJAcddND6mWh77LFHJk6cmB122KHJ7fXt29cMMgAAgC20tGNlNrfI4sKq5W94bJ/enVJZUZ6a2rosrNr0TZdv1tZ2XTuka7v+DSm1ZFZV1qQqSerrk0b8u7isflX67NA55Sn9+XXaCv/93K9fv2afQbY5d9/97zv9Dz300Ca1/2bh2DrveMc78tvf/jbLly/P/fffn0984hObra1fv35t9s758v9/4bS8oiL9+/ff6Pu25o1+/o3Rs2fPzJ07N2vWrNlomcXFixenW7duG80wmzx5cjp37pzhw4c3qq+2/PPf1rxQviZrNrOtNcbCPjtsl4r6tv05vaTy1bzxO7FpncpfTa828plQ9LHwjT4HX3755fUzjXr37p23ve1tTerjzcKxdQ477LA8/vjjSZI777xzswFZW/8cbItjYV1d3frJRTvt1PQAtzAB2fjx4zN9+vRceOGFOfTQQzNkyJAkyaRJk3LyySenqmrtA5NHjRq1Rf1cccUVefnll/Pss8/moosuymGHHZb7778/u+zS+Km5ydoHBr7+LxYAAACNc8u9c/Luz962yW2bWgbqtebc/sEM2KlLFlatyM6H/r5J/f/56h9m3Jh+TTq2tdTV1Wfosddm5vONe17baSeOzE+++mwLVdU49StXpuYDp5S6jEaZMWNGyjp2bLb2ampqct111230el1dXWbOnJlk7UXBdddFGqOh4di6bfvss0/uueeerFmzJs8//3z22GOPTbY7Y8aM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AAAAAKBQBGQAAAAAAAAUioBsK1dVVZX//M//TL9+/dKhQ4fstttu+fnPf17qsgAAAAA26eabb86oUaPSoUOHDBw4MD/4wQ9KXVKruueee3Lcccdl1113TVlZWb797W+XuiQAWslFF12UsWPHpmfPnunRo0cOOOCA/O1vfyt1Wa3qqquuyujRo9OzZ8906tQpw4YNyw9+8IPU19e3ei2Vrd4jzWbZsmU58MAD079//1x99dXZdddds2DBgtTW1pa6NAAAAICNTJ48Occdd1y+8IUv5Oqrr85DDz2U0047LZ07d85pp51W6vJaxbJlyzJ8+PB8+MMfzuc+97lSlwNAK5o4cWI+8YlPZN99903nzp3zi1/8IkcffXTuvvvu7L///qUur1XsuOOO+drXvpahQ4emQ4cOuffee/OZz3wmFRUVOfPMM1u1FgHZVuyiiy7K8uXLc+ONN6ZDhw5JkoEDB5a2KAAAAIDN+MEPfpB99903559/fpJk2LBhmTp1ai644ILCBGTvfve78+53vztJ8sUvfrHE1QDQmm655ZYNvv/e976Xv/3tb/nTn/5UmIDs8MMP3+D7QYMG5S9/+UvuuuuuVg/ILLG4FbvuuutywAEH5POf/3z69u2bt7zlLTn77LOzfPnyUpcGAAAAsJH7778/RxxxxAavHXHEEXnuuecyd+7cElUFAKVRV1eXV199NV26dCl1KSVRX1+ff/7zn7n//vszbty4Vu/fDLKt2KxZszJz5sy8//3vzw033JD58+fn9NNPz/z58/O73/2u1OUBAAAAW4mnZ8/NgkUvbfT66tWr1///noce2+j7dTp16pB9R7zlTftZsGBB+vTps8Fr675fsGBBBgwY0ORz2BKvLlueKVOf3uj1hp5/kowavke6dyvmBU6ArV1dfX0eenRa1qyp2eD1xowDu/TfKQMHbDjGvZnvfve7efnll/PpT396C6pvHs88Pz9zF7y40esNfQ86dGifMSPfkrKysjft65VXXkn//v2zevXq1NXV5dxzz80ZZ5zRTGfScAKyrVhdXV223377/PKXv0y7du2SrP1DecIJJ+RHP/pRevXqVeIKAQAAgK1Br+2656o/3ZbVr7swuM7K1Wty810Pbfb7Dx/7rhavsSV169Ipz8xZkCdnPb/J7W92/kMH7Zx3jhnR4nUC0DLKy8pSX1+/wWf7a73ZONClc8d8/hMnNKrPn/zkJ/nud7+b66+/vmQ3iLzW9j265zd/ui0rV63e5PY3ew/ef+RBDQrHkqRbt26ZMmVKli9fngceeCBf+tKX0q9fv5x66qlbdhKNZInFrVjfvn0zZMiQ9eFYkuy5555Jkueee65UZQEAAABbme17ds/R73pHk44dNXyPjBi2e4P27du3bxYuXLjBa4sWLVq/rVTKyspy/BEHpnOnDo0+tnPHDjm+ERcFAWib9nvbnhk8sGlB1fuOODBdu3Rq8P4XX3xxzj777Fx//fUZP358k/psbtt175r3HHZAk44dPnhgRu89pMH7l5eXZ4899siIESNy2mmnZcKECfnKV77SpL63hIBsK/bOd74zM2fOTE3Nv+/ueuqpp5IkAwcOLFFVAAAAwNZo3xFDM2yPXRp1zHbduuS4Q/dv8P77779/br311g1e+9vf/pZdd9215HfPd+vaOe87/MBGH/few9+Z7l07t0BFALSm8rKyvP/dB6VTx8bdLLHPiKHZc/DABu//9a9/Peedd15uvvnmNhOOrTNq+B4Z2cCbXtbp2rlT3nfEO7foRpG6urqsXLmyycc3lYBsK/aFL3whL774Yj7zmc/kySefzJ133pkvfOEL+ehHP5qePXuWujwAAABgK1JWVpb3HXFgunTq2OBjTnj3wY26kPj5z38+//znP/OVr3wlTz75ZH7961/nRz/6Uc4555ymlNzs9hq6W962V8PvgH/rnoOz91sGNaqPZcuWZcqUKZkyZUpWr16dhQsXZsqUKZk5c2ZjywWgmW3XrUujZlH12q5bjjlkbIP3/9znPpeLLrooV111VYYOHZqFCxdm4cKFeeWVV5pSbos47tD9G3Xjx/uOPDBdOzd89ty5556bv//973nmmWfy1FNP5ec//3kuvPDCnHLKKU0pd4uU1dfX17d6r7yh6urqdO3aNcnaX5q6dNn8A17vuOOOnHPOOXn88cfTp0+fnHDCCTnvvPPSubM7lwAAAIDGmzpjdq76821vut/+++yVY5qwLONNN92UL3/5y3nyySfTp0+fnHnmmfnv//7vppTaIlauWp3/vfLavPzqsjfcb7tuXfL5U09Ixw7tG9X+XXfdlXHjxm30+kEHHZS77rqrUW0B0DKuvv6OPDZ91hvuU5bkP046NgMH9Glwu5ubZXXKKafkV7/6VSMqbFlPPzs3V/zh5jfdb98Rb8nxRzZu9vXnP//53HDDDZk3b146duyYQYMG5ROf+EROO+20VFRUNKiNxmQob0RA1gY11w8XAAAAoCn+ePNdefjxGZvdvuP2PfJfp7wv7dpVtmJVreeZ5+fn51ffmDe6aPapDx6d3Xft12o1AdB6lq9clf+94tq8uqx6s/sc9PaROfLgt7diVa3rr7ffn388MnWz23v16JYzP3Z8OjTyRpHm0FwZiiUWC2LqjNmZs+CFUpcBAAAAbAWOedc70nO7bpvcVl5elhOPPmSbDceSZNAu/fLOMSM2u/2AffYWjgFswzp37JATjjpos9v77rh9Dj1gn1asqPUdefDbs0Ov7Ta5raysLB84alxJwrHmJCArgNWr1+RPt96TH//mL5nxzJxSlwMAAAC0cR07tM8JRx2cTS0ENX7/0enfp3er19TaDn3nPtmp98bPeN9x+545/KB9S1ARAK1p8MABecfovTZ6vaKiPCcePS6VlQ1bDnBr1b5dZT5w9LiUb2JZyIPePrJRS0u2VQKyZlJbW5urrroqhx12WHbYYYd06NAhu+yyS4444oj84he/SG1tbclqe/DRaalevjK9enTL7rv2L1kdAAAAwNZj0M59N5pFtUu/nXLQfqNKU1Ara1dZmROPOSQV5f++fFZRXp4TjxmXdpXb7uw5AP7tyIPGZIdePTZ47fADx6TPDr1KU1Ar27nvjjlk/7dt8FrfHbfP+ANGl6ii5iUgawavvvpqDj300Hz0ox/N7bffnvbt22fkyJGpq6vLbbfdlk996lNZunRpSWpbvXpN7v7nY0mSQ8a+LRUVfuQAAABAwxz2zn3XXwRceyf5wRsERtu6fjtun0Pf+e8ltMYfMDr9d9r2Z88BsFa7dpU58ehxKS9fO4tqt5375oB99y5xVa1r3Ni3Zue+OyRJKisq1s6eq9g2Zs8V5zeaFnTqqafmzjvvzIABAzJx4sTMmzcv//znPzN37twsWLAg3/nOd9KuXbuS1Pba2WNv3XNwSWoAAAAAtk6VlWsvhFVUlOeoQ8amd89NP4tkW3bgmBEZOKBPdu2/Uw56+8hSlwNAKxvQd4e86x2j06F9u3zgqIM3ueTgtqyivDwfOHpc2lVW5PAD992mZs+V1dfX15e6iK3Zww8/nH322SeVlZV59NFHs9deG69J2ljV1dXp2rVrkuTc7/8i7Tt0bGJL9VlavTz19UmnDu1LFtIBAAAAW7eamtpUVJSnrGAXBdepq6tLkpQXaPYcAP9WX1+f2tq6bf65Y2+kLf0usHrVypx31ieTJBdd9rt84dMfblI7FkzeQn/5y1+SJEcddVSzhGOv92r18rRfs+XPL1uxanVWrFrdDBUBAAAAAACUxurVq9Z/vWzZiia3IyDbQtOmTUuSjB07tkXa796lcxNnkJk9BgAAAAAAbFtWr/r3TL6uXTs1uR0B2RZ69dVXkyTbbdcya3Cf/R8fTJcuXRp93D0PPZab73oovXp0y1mfPDEVFZYAAAAAAAAAtm7V1dXrl1j8z5OOa3I7nkG2hY4//vj86U9/ygUXXJAvfvGLzdJmfX19li9fnp/+7q9ZtbquCWt6mj0GAAAAAABse+rr67Pm/y+z2KtXj5zxseOb1I4ZZFtozz33zJ/+9Kf84x//aLY2y8rK0qVLl6xeszbo2hKePQYAAAAAAGyLllWvbPKxArIt9N73vjff+ta3cvPNN2fatGkZPnx4s7XdrUlrZ5o9BgAAAAAAbPualqOsZYnFZnDiiSfmD3/4Q3bZZZf85je/yUEHHbR+26JFi3LllVfmjDPOaNKzxBrLs8cAAAAAAADemICsGbz66qs57rjjctdddyVJ+vfvn379+mXBggWZN29e6uvrs2TJkvTo0aNF61i9ek0uvOzqVC9fmfcfeVD2GTG0RfsDAAAAAADYGple1Ay6d++ev//977niiity8MEHZ/ny5XnsscdSXl6eww8/PFdccUW6devW4nU8+Oi0VC9fmV49uuWtew5u8f4AAAAAAAC2RmaQbUOen/9Cbr93ckYO293sMQAAAAAAgM0QkG2D6uvrU1ZWVuoyAAAAAAAA2iRLLG6DhGMAAAAAAACbJyADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFUlnqAgAAAJpLfX19lq+oKXUZjdK5U2XKyspKXQYAAEChCMgAAIBtxvIVNem6329KXUajLHvwo+nSuV2pywAAACgUSywCAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACiUylIXAABA21VfX5/qFTVZtbo27SrL06VTZSoq3GMFAEXhdwEAYFslIAMAYL36+vrc8/DC3HzvnDw87aU8PK0qLy9dvX57546VGfWWXhk9vHfe9fZ+OeqdO6ey0kUyANhW1NfX54EpL+TGe57Pw9Oq8vC0l7L4lVXrt3fqWJGRQ9b+LjBu37459uBd066d3wUAgK1PWX19fX2piwAAoLRWrKzJlX+ZkZ9cMz3TZr3c4OMG7NQln37/0HzmxGHZvkfHlisQGqh6+Zp03e83pS6jUZY9+NF06dyu1GUABbdqdW1+9den85NrpudfMxY3+Lg+vTvlU8cPzekfHJ4dt+/UghUCADQvARkAQME9+NgL+djX7slTs19pchs79uqYn31t/7z3XQObrzBoAgEZQOM9PK0qH/vqPXli5pImt9Fruw659Etj88EjB6WsrKwZqwMAaBnmwAMAFFRdXX2++qPJ2f+UG7coHEuSFxavzPs+f0c+8qW7snJVTTNVCAC0pPr6+nz78kfz9pOu36JwLEkWv7IqHz7nrpxw1sQsW76mmSoEAGg5AjIAgAKqq6vPJ79xb77z88dSV9d8Cwr87qZZOfIzt6XahTEAaNPq6+vzX+f/I1+79JHU1jbf7wLX/X12Dv30LXnlNc8wBQBoiwRkAAAF9F/n/yO//MvTLdL2XZMW5L2f/3vWrKlrkfZpfi8uXpGXX121RW3MfP7VZg1bAWhZ5/zvpPz499NbpO0H//Vijvmv28wqBwDaNAEZAEDB/PG2Z/OTa1rmgtg6t/9jfs6/4rEW7YPm8cJLK3LIJ2/Jof/xtyaHZP94bFHeduJfcvp3HxCSAWwFbrrn+Xzvl4+3aB/3PrIoX//xIy3aBwDAlhCQAQAUyAsvrchnvvNAo4+bdPWxmXP7BzPp6mMbfMy3Ln80jz31UqP7onV9+Jy78sTMJZk8tapJIdk/HluUw0+7NUur1+Snf3iyxcNXALbMkldX5dPfvL/RxzXld4Hv/+aJPPjYC43uCwCgNQjI3kBVVVUmTJiQPfbYIx07dszOO++cM888M9XV1Tn11FNTVlaWSy+9tNRlQouaNefV/PmO2fm/m2bl1vvnZvkKS2QUSX19MnVJ8vf5yW3zkikvJSYGwNbtnB9OStWSlY0+rk/vzhmwU5f06d25wcfU1NTnP755f+rrfXC0ZT/84n7ZsVfHJGl0SPbacCxJxu/XL6e+d0iL1QrAlvv6jx/J/BeWN/q4pvwuUFdXn0+dd5/fBQCANqmy1AW0VVOmTMmRRx6ZhQsXpkuXLhk+fHjmz5+fSy65JLNmzcrixYuTJKNGjSptodBCbrz7+fzo6mm57YF5G7zeo1v7fOy4wTnzpD0zsH+3ElVHS1tTl/z1ueSPs5NZSzfc1r9zcvzAtf91MYrAVuXFxSvyu5tmtWqfDz3+YiY9UZUxe+/Qqv3ScHvu0TMTf/HuHPLJm/PC4pXrQ7LbLzsiPbp32OxxmwrHrr/k0HTquG0MDt85Y3S+/MlR+cTX79nk8/ruvOLdGTtyx4z+4F8zdeaSElQI0HivLF2dX/5lRqv2+cTMJbnznwtyyNv7tWq/AABvxgyyTaiqqsoxxxyThQsX5qyzzsqCBQvyyCOPZOHChbnwwgtz0003ZdKkSSkrK8uIESNKXS40q/r6+vz3RQ/mmP+6faNwLEleXro6//vbqRn9wb/m/kcXlaBCWtqyNcl/PZhc8PjG4ViSzFueXDIt+eR9yYuNn4QClNAVf56R1WvqWr3fH/9+Wqv3SeOsC8kaOpNsWw/HkuQbP3k0jz+9OD/4wtvTf6cNZ0t87iN75uB9++bcnzwiHAO2KlfdODPVJVgVxPK7AEBbJCDbhDPOOCNz587N6aefnosvvjjduv17lsyECRMycuTI1NTUZODAgenevXsJK4Xm9/UfP5L/uWrqm+63+JVVefdnb3VRaBtTU5dMmJRMrnrzfZ9+NTnjwbWBGrB1+O2NM0vS7x9uezar19SWpG8arqEhWRHCsSRZU1OXU756T7p0apcrvvHO9a8PGbhdvvNf++TBf72Qi371eAkrBGi8Uv0u8Jc7n8vS6tUl6RsAYHMEZK8zffr0XHPNNendu3fOP//8Te4zevToJMnIkSM3eP3ZZ5/Nsccem27duqVnz5756Ec/mpde8mB6th7PzV+a7/x8SoP3f3XZmnzxfye1XEG0ur/PT/7ZgHBsnadfTa6d3WLlAM1o2fI1mfbMyyXpe+Wq2jzxtBsqtgZvFpIVJRxb59HpL+X8Kx7L4fsPyKeOH5ry8rL85jsHpqwsOeWr96TOgzmBrcjqNbWZ8tTikvRdW1ufKU+Wpm8AgM0RkL3O1Vdfnbq6upx00knp2rXrJvfp1KlTkg0DsqVLl2bcuHGZO3durr766lx++eW59957c/TRR6eurvWXMoKmuPzap9LYZyfffO+cPDt3E+vwsVVqStj15+eSWtcHoc2b8uRLjf6Mb06TpzUifaekNheS3fbA3EKFY+t86/JHM+XJl3LxWWPyoy+Nzdv33jFf+dHDmTH7lVKXBtAoU2cuyarVpZvR7XcBAKCtEZC9zsSJE5Mk48aN2+w+c+fOTbJhQHb55Zdn3rx5+ctf/pKjjz46J5xwQv7v//4vDz74YK6//vpG1VBfX5/q6upUV1envpRXsiic39zQ+OU26uuT391cmmU6aF7zqpMpTbipc97yZIrJstDmlWr22Pr+Z5W2fxpnUyHZkZ+5rXDhWJLU1NTnlK/ek44dKvKZE4fl3kcW5n9/+0SpywJotNL/LmA2OQDQPJorQymrl8BsYOedd87cuXPz6KOPZtSoURttr6mpSd++fVNVVZVZs2Zl0KBBSf4dqN15550b7L/77rvn4IMPzhVXXNHgGqqrq9fPXuvbt2/Ky+WYtLz6lGV+z68nZY3/89Zl5aT0WH5jC1RFa2q3+77Z/qw/N+nYl688PSsn/6V5CwKa1bIOY/NKlyM2uW3S1cemT+/Ob3h8n96dUllRnprauiysWrHZ/RZWLc++H9r45qDOKx9Oz+WNu2mI0ltTsUNe7PaJ1Jf/+89H+zXPpvfS36YsNSWsbPPq0i4Len21WdvctV/XPH7de9OtS/t86YeTcsEV/2rW9vsu/nbK46GeQMuq7jA6L3c5dpPbWuN3gU6r/pVe1dc1rmgAgE2oq6vLggULkiSjRo3Ko48+2qR2tv1bPhupuro6SbJixaZ/2bvmmmtSVVWVbt26Zbfddlv/+rRp03LCCSdstP+ee+6ZadOmNbmedT9kaBU9m5aXV1cvS/X8ec1cDK2tS7dds30Tj128uCpL5vkzAG3a9i8nXTa9qU/vzhmw02Y2vk5lRXmD932t5dXLstxYsfXp3CHpuuHNM6vXlGX+gheSus1fHC2psvZJr+Zt8pfffGfat6vItFlL8tVPj8ofbn02zzTjEtML5s9P6lc3W3sAm9RzUEl/F1ixYnnm+TcDANDMFi1a1ORjBWSv06dPnyxZsiSPPPJIxo4du8G2BQsW5Oyzz06SjBgxImVlZeu3LVmyJD169NiovV69euWpp55qcj1mkNGaFta9ktqKxl9R6t6pJt3692+BimhN5ZVrL8zV19dv8Pn2Rtbtu1398nT2ZwDatOr2nfLyZrYtrFr+psc35q7xTenSpV16+JzYqqyqHJCXup2c+rKOG27oPDDthp6T3kt/k/L6laUp7g3UpV2a8xaz//rw8Iwb0y9fvmRy/nrnc3nkmvfkym++Mwd/4uZm66Nvv35mkAEtbnn7ztncIoet8btA546V6el3AQCgGbx2BtlOO+3U5HYEZK8zfvz4TJ8+PRdeeGEOPfTQDBkyJEkyadKknHzyyamqWvtQ2U0tv9gSnn766XTp0vg7s6Apvn35o/napY806pjKirJMf+CK9NvRn9Ntwen/SB58sWHhWJKUlZVlYNdk0j3Xp4GZGlAiD/3rhez3kRs2uW1TyyC93pzbP5gBO3XJwqoV2fnQ3ze6/x9974v5+HsavuQ0pfWPxxbl8NNuTf3/f+ZYh3blWbWmLuVlSV19sqayf/of8P3cftkR/4+9+w6PourbOH7vplcCCZBA6D200IsoRRAQATsiYkOxgKCi2BUrotgQC3axIAqoFFFBQIqCoSkl9CKBJBBIIKSQsvv+wQMvYAKZzfb9fq6L63nMzpnz281mdnbuOecoKjLIxdWeLSe3UOEdp9plX/VrRmr86Lb6a8MhTfjkH1ksVo17b63Gj26n+25M0Ntf2z5TxJm2b9umsNAAu+wLAEqzcfsRNb+m5CnVnXEuMOHZ+zRy8HuG2wEAAJzrzGWqli9fbvN+GJp0jrFjxyo6Olr79u1T06ZN1bx5czVo0EDt27dX3bp11aNHD0lSy5Ytz2pXsWJFZWVl/Wd/R44cUaVKdp7jBXCQO65uJH8/YynHVZfWJhzzItfWtq0N4Rjg/lo0rCQ/g8d4e2qTEOOyvmHMqXAs+3/hWM+O1RQddTIEi6kYrCqVTo4oW70pQ73u+llZx064rFZHMpmkz56/RH5mk2558ndZLCenon7l0w1K2nhI40e3Vd34CBdXCQBl17hOlEKC/VzWf5sEWyd0BwAAcAwCsnPEx8dr2bJl6tevn4KDg7Vnzx5VqlRJU6ZM0bx587Rt2zZJ/w3ImjRpUuJaY5s3b1aTJk2cUjtQXrExoZo4pn2Zt68aHaJXHmjnwIrgbJfESj2rlX37xErSVbUcVw8A+wkJ9leLBq65aSc8NEAJdaNc0jeMKSkcmz2p1+mpdwP8zVr00eU+EZKNuaW5LmpVVU+/u1Zbdh89/XOLxapbn1oqfz+zPnnuYhdWCADG+Pub1dZFN6wEBpjVsiEBGQAAcC8EZCVo0qSJ5s6dq+zsbGVnZ2vVqlUaPny4cnJytGfPHpnNZjVr1uysNldccYWWL1+ulJSU0z9btWqVdu7cqf79+zv7KQA2G31TM718f9sLbhdfNUwLP+ij2tW5c9qbmE3Ss62kS+MuvG3raOn19lKQ625CBWDQrQMbuKTfm/rVk78/p53urrRwLCT47FnZm9av6PUhWeM6FfT8iNb68++Deu3zjf95fPPOLI17b626to3TfTcmuKBCALDNrQMbuqTfQb3rKjSEVT4AAIB74UqFAZs2bZLValWDBg0UGhp61mPDhw9XXFycBg4cqLlz52rGjBkaPHiw2rdvr4EDB7qoYsA2j9zeUqu+GqCb+9dXUODZ6UftauF6+f62Wv/dlWrmopEIcKwgP2l8W+nVdlKHyv99vHnFkyHaO52kyEDn1wfAdjf3r6/QYOdfnLpnEKPp3V1Zw7FTvD0k27L7qELafa7OQ+ecnlrxXC9//I9MLT622zpkAOAMN/Spq6gI55/E38u5AAAAcEMEZAZs2LBB0n+nV5SkyMhILVq0SHFxcbrhhht0xx13qHPnzpo7d67MZl5meJ72zSvr8xe76sBvgxVT8eS6I5UrBmvHvOv0yO0tFR0V7OIK4Uhmk9Q97mQINqendGrVIpOkTy+W+tWQAji0AR4nKjJIt1/l3DvHL+1QTS0ackOFOzMajp3i7SEZAHij0BB/3X19Y6f22aF5ZXVoUcKddwAAAC7G5U0DzheQSVK9evU0d+5cHT9+XFlZWfryyy9VuTIngfBslSoEKSjg5CiywACz/Pw4bPiauNCzAzIAnu2FkW0UXzXMKX2FBPvp/acuckpfsN3zU9YbDsdOKSkk+/bX3Q6rFQBQfk8Nb6X6NSOd0ldggFkfjetyei1LAAAAd8KVbgMuFJABAAC4uwoRgfpoXBfD7dIycpWSnqO0jNwytxk/qq3TLsDBdtNf7a7OiVUMh2OnnBmSPXN3Kw2/1rkjEwAAxoSG+OuTZy+W0czKlnOBZ+5uxdT8AADAbbFCqgGLFi1ydQkAAADl1vuieD19Vys9N2Vdmdu0GzzbUB+D+tTRfTc2NVoaXCAiLFA/v9db/n5mw+HYKU3rV9SGmVerSnSInasDADjCxW1i9coD7fXw63+VuY3Rc4EB3Wpq7G0tjJYGAADgNIwgAwAA8EHj7m2lR253zEWrqy+trakvdpXZzHRKniIiLNDmcOwUwjEA8CwP3dpcz41o7ZB9X35xvKa/2l3+/lx2AgAA7oszFQAAAB9kMpn08v3tNOnRjgoO8rPTPqUxNzfT9Fe7KzDAPvsEAACO89RdrfThM10UWs6bJM5076Am+v7NngoOYtIiAADg3gjIAAAAfNh9NzbV+m+vVKeWVcq1n/o1I7X0036a+FAH7hYHAMCD3HFNI22YdZW6to0t135qVQvXbx/21TtPdOZGGQAA4BG4nQcAAMDHNaoTpWWf9dP3v+3Ve98ma9FfqWVum9i4ku69vomG9Kuv0BBOLQEA8ER14yO16KPLNXvJXr337Rb9+sf+MrdtWi9K9w5qopsHNFB4aIADqwQAALAvrmIAAABAfn5mXXtZHV17WR0l78rSzytStGZzhlZvylBKeo5y8opObmc2aWD3WmqTEK1LO1RT++aVZTKx1hgAAJ7ObDbpyh61dWWP2tq+96h+WrZPazYf1prkDO09cPysc4H+3WqqTUK0urWN00WtqnIuAAAAPBIBGQAAAM7SpG6UmtSNOutn8T2naf/BXMXGhGjmG5e6pjAAAOAUDWpV0OhaFc762ZnnAt+/2dNFlQEAANgPC0QAAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKf4u7oAAAAAALCX0BB/HV95s6vLMCQ0hK9lAAAAAOBsfBMDAAAA4DVMJpPCQgNcXQYAAAAAwM0xxSIAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHyKv6sLAAAA7md/eo5Wb87Qms0Z2rb3qPLyi+XnZ1KF8EC1bFhJbRJi1KpJtMJDA1xdKgDYndVq1Z79x7Vmc4bWJGdo575snSgolr+fWRUjA9WqSbTaNIlRy0aVFBLMVyoAgPcpKCzWxu2Z//ssPKyMzHwVFFoUFGhWzbhwtWkSozYJ0WpQq4LMZpOrywUAwCZ8mwMAAJKkEwXFmrFgt96dnqw/1h+84PbBQX4a3Leu7h3URG2bVnZChQDgWDm5hfr6p51699tkrd9ypPQNvz/5PxFhAbq5f33dc30TNa1f0TlFAgDgQLtTsvX+d8n6+PttOpx14oLbN6gVqXuub6JbBzZQxcggJ1QIAID9EJABAAB9+8sujXp5pdIP55W5Tf6JYn36w3Z9+sN2dWsXpw+f6aL6NSMdWCUAOIbVatWHM7fqkTeSlJVdUOZ22TmFeuebZL3zTbIGdq+pd5/orGpVwhxYKQAAjpF57ITGTFylz37cLqu17O227z2mB19dpSfeXq2nhrfSw7c2l78/K7oAADwDn1gAAPiwQ0fydN2Y3zTo4cWGwrFzLUlKVYtrZ2nSV5tksRj4Rg0ALvZv6nFddtfPuuu5FYbCsXP9uPhfNb1qlr6Ys92O1QEA4Hg/LdunZlfP0qc/GAvHzpSXX6zHJ61Wp6FztGlHpn0LBADAQQjIAADwUbtTstXxpjmasWCPXfaXl1+s0RNWatgzy1RUZLHLPgHAkf7ZdkTtb5ythSsP2GV/WdkFuvmJpXr0zSRZbb3CCACAE036apP6jfhVBw7m2mV/qzdlqONNc7QkKdUu+wMAwJEIyAAA8EH70o6r6+3ztCsl2+77/uzH7bpj3HJGkgFwa5t2ZKr7sJ/KNXq2NBM++UePvbXa7vsFAMCeJn21SaMnrLT7fo/nFuryEb9o2Zo0u+8bAAB7IiADAMDHFBQWq/99C7QvLcdhfXw+e7te/vhvh+0fsKeDh/PUY9hP+mfbEZv38cf6dPW++2cdLccUfXCeo9kFunzELzpy9ITD+pjwyT/69IdtDts/AADl8cuKFIeEY6fk5RdrwKgFSnHgdw4AAMqLgAwAAB/zwgfr9fdWY0FA0rQB2rfgBiVNG1DmNuPeW6cN5QgcAGc4dCRPl945X4uTUnXpnfNtCsn+WJ+uPvf8ol//2K8+9/ysY8cJydzdQ6+t0r+pxi7Y2XIcvP+VldqXdtxoeQAAONTR7ALdMW65oTa2fA5mZRfozmeXM+0wAMBtEZABAOBD1m85rJc+Mj6yKzYmVPFVwxQbE1rmNoVFFt361FIVF7MeGdxXYICfwkL8JUkZmfmGQ7JT4Vh2TqEkKTw0QAH+nGK7swV/7tdHs4yP7LLlOHjseKHuem6F4b4AAHCkh1//Synpxm4UseVzUJJ+XpGiz2dvN9QGAABn4dv7eWRkZGjs2LGqX7++goODVaNGDY0ePVo5OTkaNmyYTCaTJk+e7Ooy4SD703M07t21qt/vW4W2/0wRHaeqxTWz9NaXG5V1zHHT8cB9bDsqvfi31PsXqfNcqft86e4/pIUHpCKu98NDvfzJ3youdt4dnGuTD2v+8hSn9QcYVSEiUL+830cdmleWZCwkOzcc69mxmmZP6qWQYH+H1ozyeeGD9U7tb/7yFK3edMipfcI+rFarFq7cr6sfWKiYS75UcNvPFHPJl7puzG9a/NcBRkT4gPwTRfpiznZddPMcRV30hULafab4ntM0+uU/tWV3lqvLgxMcOpKnCZ/8rSYDZyi8w+cKa/+5Gg+YoZc//luHjth/DUtnSEnL0SdOngL4hQ/Wsz4xAMAtEZCVYv369WrevLleffVVpaWlKSEhQYWFhZo0aZIGDRqk5ORkSVJiYqJrC4XdWa1WjXt3rWr1ma5n31+nnfuylZdfrOO5hdqwPVP3v7JK1Xt+o6ncAeW1coukh/+Sbvxd+n6vdPiEVGCRsgul1RnSo6ulgb9JyVmurhQwJi0jVzMX7nF6v+9OT3Z6n4ARtoRkhGOeaeP2I1q6Js3p/b737Ran94ny+Tf1uNoM+lG9hv+s73/bq8NZJ3SioFiHs05oxoI96nHHfHUYMlsHDrK2jrf6fXWqavWerpufWKo/1h/U0ewC5Z8o1v6DuZr09WY1GThTtzzxu04UFLu6VDjIpK82Kb7XN3r0zdXasvuocvKKlJtfpK17juqxt1Yrvtc3evOLja4u07APZ2516g1zkrRzX7YW/LnfqX0CAFAWBGQlyMjIUP/+/ZWWlqYxY8YoNTVVa9euVVpamiZMmKB58+YpKSlJJpNJLVq0cHW5sLMxE1fp2ffXnfeEMTe/SLc8uVQfzuBih7fJL5ZGrZQWX+DaWXqeNHyFtDnLKWUBdvHpD9tVVOT8Ozd/XpGivQeynd4vYISRkIxwzHN9MHOrS/qdNn8na9N5kJS0HHW5Za7WbTl83u2SNmaoyy3zlJaR66TK4CyLVh3QZXf9rINH8s+73dQ5O3TNg7+piOklvM6ET/7W6AkrVVBY+u+2oNCiB15dpRedPDK5PCwWqz6c5ZrPwilcPwEAuCECshKMGjVKKSkpGjlypCZOnKiIiIjTj40dO1YtW7ZUUVGRateurcjISBdWCnv7/rc9euOLTWXe/p4X/9CmHZkOrAjO9l6ytL6MS8/kFUsP/SWd5zsT4FaWrE51Sb9Wq7RsbbpL+gaMKEtIRjjm2ZYkueY4mJdfrKSNGS7pG8YNfeJ37Usr28iw3fuzdeuTSx1cEZzp2PECXfPgb+cNRs40b+k+vfLpPw6uCs60Yl26Hn1zdZm3f3LyGi110Xm2Udv2HlXqIdeE+r+vTmNqWgCA2yEgO0dycrKmT5+umJgYjR8/vsRt2rRpI0lq2bLl6Z+dCtTat2+voKAgmUwmp9QL+5r09WZD2xcXW/Xet0wd5i1yi6Qf/jXW5mC+tNgzvgvBx1mtVq3Z7LqLs67sGzDifCEZ4Zhny8sv0uZdWS7rn+OgZ/h762HDQeovf+zXVtaj8hpfzN2hrGxjIz7fmZ6sQu6a8xpvTyv7TbP/38bYtQRXceVn0ZGjJ7T3wHGX9Q8AQEkIyM4xbdo0WSwWDRkyROHh4SVuExISIunsgGzHjh2aOXOmYmNj1a5dO6fUCvtK3pVl013FU+fs0PHcQgdUBGf7Zb+UU2S83Yw9di8FsLuU9Bwdzjrhsv7XJp9/mirAnZQUknW9bZ563fUz4ZgH27A90+lrrpxp7RYCMk/wvo3rxb3/HVOHeYv3bFg79cDBXM353eCddnBL6YfzNHPBHsPtvl+01yPWJFzn4nNyvhMAANyNycr45rN06dJFK1as0A8//KCBAweWuM2VV16pH3/8UbNmzdJVV10lSbJYLDKbT+aN48aN07PPPmvz0PGcnJzT4VxcXNzp/cKxcgObKzP8WpvaVjn6rgKKvXf6sNSoB2UxV5DZclRxWa+7uhyHibzhJYVecrPhdpa8bB0c08QBFbmPqpP3ymT2k9VSrPSRtVxdDmxQ4BerQxXuKfGxpGkDFBsTet72sTEh8vczq6jYorSMvFK3S8vIVbvBs//zc/+ig6p67B1jRcOt+MpnwZkspiBlRAxVoX+Ns34eVLhT0dlfyyQb7qrwUN7w+88PaKDDETeV+JgzjoNBhTsVkz3VWNFwuoORd6rQP95wu8DC3aqc/Zn9C4JTWeWnA5WetqltRN7visxbZOeK3Is3fBZcSL5/HR2OvNWmttHHpiq4aKd9C7KzzLArlRvUqsTHLvRZWNbPQan0z8KonNkKO7HGWNEAAJTAYrEoNfXkYJfExEStW7fOpv1wy+s59u7dK0mqVavkC8BFRUVasWKFpLNHkDkqxDr1S4YTVKwjlTxo8IIOZmRJufvtWo5biSiWzJKluFj793vv8/QvtOj8l8ZKZgoM8erXRZKqngr8rVavf65eKyRQqlDyQ7ExoYqvGlam3fj7mcu87ZmKii28dzydj3wW/Ef4d1Lt+yXT/871rFad2DNNB/L3urQsp/OG339EZSmi5IeccRw8caLIc187XxIim74lFxSK3683MIdIlWxrmn38hLLTvPw94A2fBRcSES3ZuNT84cxj0jE3f11qnJCCSn6orJ+Ftn4OSlJWVrayjrj5awQA8Djp6bYPXCEgO0dOzskh8Xl5Jd8NM336dGVkZCgiIkJ16tRxeD2MIHOevIBgHTHayGqVTCZViY5QQMXqjijLLaT6+ckiyeznp7jq3vs8Q2TbVJnW3KOq7sWviyTp1LqKJpP3P1cvVehXUQdLeSwt48ILdRsZOVESfz+pKu8dj+YrnwVnOuFfQ4cjbpLVdMa5mMkkc70HFJP9uVePHj+XN/z+8wMiVdrETs44DgYF+SnGQ187X3LIv0jGVp86KSigmN+vF7DKpANWy//fFGFARJifIr38PeANnwUXcsI/VLZOiBtTMVRBEe79umSGBKq0T7wLfRYaHUFWkqgK4QoLce/XCADgGc4cQVa1alWb90NAdo7Y2FhlZmZq7dq16tSp01mPpaam6uGHH5YktWjRQqZTF4wdaPv27QoLs+3OHBiTeeyEqvecprz84rI3MpnUoFaktqxfJ7PZ8e8HV4nvOU37D+YqLjZOKRtTXF2Ow/x1SLr3T+PtBiZE65kU731dJKn9bMkiyc/spxQvf67e6mh2gaIu+qLEx0qa/uRc+xbcoPiqYUrLyFONXt8Y7r/vpe01++0nDLeD+/CVz4JT/lifrj73/CLr/9YcC/Q3qaDo5GhaizlMltoP6qcP+6pFQxuHGngYb/j9b92dpcYDZ5b4mDOOg7cM7qcpT4833A7O9eqn/2jsG0mG2018eqhGDub36w0G3LfApvXEls59W4mNox1Qkfvwhs+CCzlRUKyal32jg0fyDbWLqRisfUm/KzjIvS+zPT9lnZ5+Z22Jj13os7C8n4OSNOPr93Vpx2o2tQUA4ExnLlO1fPlym/fD0KRz9OzZU5I0YcIEbdu27fTPk5KS1L17d2VknLyXKDEx0RXlwYEqRgbpxr71DLe75/omXh2O+ZJ2MVItG6bZvM7xg0mBcqsQEaj6NW2cL8YO2jaNcVnfgFGnwrHs/4VjPTtWU0zFYElSgP/J0+eMzHxdeud8/bPN8PhzuEiDWhUUERbgsv7bJnAc9AS3XdlQQYF+htqEhfhr6BX1HVQRnO3eQcbXFu6cWMXrwzFfERTopzuubmS43bCrGrp9OCZJbVz8WdQ6gb8TAIB7ISA7x9ixYxUdHa19+/apadOmat68uRo0aKD27durbt266tGjh6Sz1x+D93jo1uYKCyn7SW2N2DDdOrCBAyuCM5lM0p0NjbXpUlVKiHJIOYDdtXHhF1JX9g0YUVI4NntSr9MzB8REBalD88qSCMk8jdlsUisXXsB29UVJlE1MxWDdNzjBUJsHhjZThYhAB1UEZ7usc3V1almlzNubTNJTw1s5sCI424gbmpy+MaYsoqOCNPIGY8cNV3HlZ1G9GhGqGFnKAmgAALgIAdk54uPjtWzZMvXr10/BwcHas2ePKlWqpClTpmjevHmnR5URkHmnxnWiNPP1SxUSdOG7RqtGh2j+u705wfMyfeKlexuXbdtmFaUXWju2HsCeBnar5ZJ+I8MD1LVtnEv6BowoLRwLCf7/m2fMZpN+eb8PIZmHGti9pkv6rRkXppaNfGM6Tm/w8v1tdX3vsk0RMPSK+nr2Xk4IvYnZbNKPb/VU03pRF9zWZJLeebyz+nSJd3xhcJpqVcI09+1eiipD8B0ZHqA5b/dSfKxnLI1RNTrk9DmMsw3o5prPYAAAzoeArARNmjTR3LlzlZ2drezsbK1atUrDhw9XTk6O9uzZI7PZrGbNmrm6TDhI74viteSTfrqkTWyJj5vNJg3sXlN/ftFfTetXdHJ1cIbbG54MvmqW8h0n1E+6vo70ficp3HUzNQGGXd2ztqpUKvvdsPZyc/8GCg/ljwXurSzh2CkVIgIJyTzUrQMbKrgMN0LZ213XNpafH1+9PIWfn1nTJnTXCyPbqHIpo0hiY0I04f52+uyFS5hu3QtVrhSi5Z9f8b9p80o+ZjRvUFE/vNlT99gwJSPcX4cWVbRi6hXqc1Hp4edlnatrxedXqFPLqk6srPxsmUbUHu65nr8VAID74VuaAZs2bZLValWDBg0UGhr6n8dnzJihGTNmaPPmzWf99+rVq51dKsqpffPK+v3Tftow8yo9cnsLhQSf/FIUERagPT9frx/e6qU68REurhKO1CdemtFDeqfTyTDsFJOkny6TxjaXSrhmCrg1W9dUKK97ri/jsEzARYyEY6cQknmmShWCNLhvXaf2GeBv1jAXHHtRPmazSU8MT9S+BTfo65e7nZ6GPSzEX9Nf7a5/f7lBY29vQTjmxaIig/TRsxdr/8LBeuPhDgr933fCsBB/Lfusn/6ecZUGdHfN6Hw4R0K9ipr/Xm9tn3udnror8fR7ICLUX9vmXKtf3u+jZg08b3Tw9b3rKDrKuTPh9OpUTQ1qVXBqnwAAlAUBmQEbNmyQVPr0itddd52uu+46fffdd2f99+TJk51WI+yrWYNKevn+dqr0v2kUI8MCVCM23MVVwVnMJqlD5ZNh2KmDpUmMGoNne/jW5qpW5b83eTjKXdc1VkI9RtvCfe3cd8xwOHZKaSFZ+uE8h9aM8nn23taKdOKH+RN3tlTV6BCn9Qf7Cgr00+DL652eai0qIlDX966rgAC+SvuKShWCdP/QZqen1o+KCFSX1rGn16aE96tfM1LPjWhz+j0QGR7o0WFPcJC/Jtzfzmn9+fub9OqD7Z3WHwAARnBWb8CFAjKr1Vriv88++8yJVQIAULqoyCB9+EwXp/RVq1q4Xn3QeV++AVvUjY/QLQMaSDIWjp1ybkg2YlATwhA3VyM2XK8/1MEpfSU2rqTH70h0Sl8AAJTV7Vc1VO/O1Z3S11PDW6llo2in9AUAgFEEZAZcKCADAMATXH5xDcPTHqZl5ColPUdpGbll2j7A36zPnr9EEWEXXtwccCWTyaRJj3bUu090NhyOnXIqJPvwmS4ad29rB1QJe7v9qoa6soexqdGMHgfDQwP0+QtdGWkEAHA7JpNJH47rYmh9YqOfg5LUqWUVPTaMa2gAAPfFCjoGLFq0yNUlAABgF5Me7aRDmfmasWBPmbZvN3h2mfft52fSl+O7qlu7OBurA5zLZDLpnnIuWF8hIlB3XMM6U57CZDLp65e7qd+IX7U4KbVMbYwcB4OD/PTjWz3VoqHnrU0DAPANNWLD9euUPuo+7CdlHiu44PZGPgclqUXDSpo7+TJuFAEAuDU+pQAA8EH+/mZNm9Bdt1/V0K77DQn204zXeuj63nXtul8AsLeQYH/Ne+cyDehW0677jYoI1C/v9VaPDtXsul8AAOytZaNo/f5JP1W38xrFnROraPHHl6tShSC77hcAAHsjIAMAwEf5+5v10bgu+uaV7oqOKv+X14tbV9U/M67WlT1ql784AHCCkGB//fBWT73/1EUKDw0o9/76XVJDm76/Wpe0ZQQtAMAzNG9YSRtmXa2b+9cv974C/M168b42+v2TfoRjAACPQEAGAIAPM5lMGtSnrjZ/f43uvKaRQoL9DO+jTvUIvfN4Jy35pJ/q14x0QJUA4Dgmk0l3XddYG2ddpcF96yrA3/hXpIR6UfpyfFfNebuXqlUJc0CVAAA4TsXIIH3+YlfNndxL7ZrFGG5vMkn9u9bU2ukD9fidifK34bMUAABXYA0yAACgKtEh+uCZLprwQDt9/uN2Tf9ll9ZvPaL8E8Ulbl81OkSdE6vojqsbqc9F8TKbTU6uGADsq1a1CH09obveGJunj2dt1azf9mrD9iMqKLSUuH181TBd3Lqqhl/bWF3bxspk4jgIAPBs/S6pqX6X1FTSxkOa8t0WLU5K1a6U7BK39fMzKaFulPpdUkN3XdtYtatHOLlaAADKj4AMAACcVjEySPcPbab7hzZTUZFFybuytG3vUd357HJlHitQdIUg/TPzKkZIAPBaVaND9PidiXr8zkQVFBZr4/ZM7UrJ1vBnlysz++RxcPMP16hKdIirSwUAwCHaNausds0qS5Iyj53QuuTDunbMb8o8VqBKkYGa905vtWxUSSHBXFYEAHg2xjwDAIAS+fub1bxhJV3Tq45C//flNzjIj3AMgM8IDPBT64QYXXtZHYWG/P9xkHAMAOArKkYGqUeHaqe/D4QE+6tjyyqEYwAAr0BABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn+Lv6gIAALAnq9Wq3LwiV5dRZqEh/jKZTK4uAwAAAAAAAPApBGQAAK+Sm1ek8I5TXV1GmR1febPCQgNcXQYAAAAAAADgU5hiEQAAAAAAAAAAAD6FgAwAAAAAAAAAAAA+hYAMAAAAAAAAAAAAPoWADAAAAAAAAAAAAD7F39UFAAAAuKuj2QXasP2IMo8VqNhiUUiQvxrUilSd6hEymUyuLg8AHO5wVr42bM/UseMFskoKDfZX4zoVFF81jOOgD7Barfo39bi27jmqvBPFMptMigwPUIuGlVQxMsjV5TlFdk6B/tmWqbwTxZKk/BPF2vHvMdWrwbmAr0g/nKdNOzKVd6JIkpRfUKy0jFzFxoS6uDIAAFBeBGQAAAD/Y7Va9cf6g/pw5latWJ+uHf8eK3G7ipGBapMQo+suq6MbL6+n8NAAJ1cKAI5hsVi1cOV+ffrDdq3856D2HDhe4naVKwarXbMYDe5bT9ddVkdBgX5OrhSOkpdfpG9/2a1p83dqTfJhZWTml7hdneoR6tSyioZd1VDd28d5TVhktVr114ZD+nDmVi1fl65te4/Kav3/xw8fPaEGV3ynChGBat04Wtf0qq2hV9RXZHig64qGXRUVWTR36b+aOmeH/tpwSPsP5p71+OGsE4rrMU3VqoSqfbPKurl/ffXvWlP+/kzSBACApyEgAwAAPs9qtWraT7s04dN/9M+2IxfcPvNYgRauPKCFKw/o4df/0m0DG+rpu1upUgXfuJsegPexWKz6YMYWvf7FRm3fW/LNAWc6lJmvn5al6KdlKXrg1VW669pGemxYS4Vxw4DHys4p0Isf/q0PZ27VkaMnLrj97v3Z2r0/W1//tFON61TQmJub6/arGsps9sygzGq1atbCPRr/8T9asznjgtsfzS7Q4qRULU5K1aNvrtbQK+pp3D2tVSU6xAnVwhEKCov15heb9Pa0zUpJz7ng9gcO5uqHRXv1w6K9ql4lVPfdmKAHhjZTYAA3DAAA4Cm4vQUAAPi0Awdz1P++BRry2JIyhWPnOna8UG99tUlNr5qpOUv+dUCFAOBYO/49pq63zdM9L/xRpnDsXBmZ+Xrxw7/V4trv9fvqVAdUCEdbuHK/ml09SxM++adM4di5tuw+qjufXa6ed87X7pRsB1ToWAcP5+m6MYt07ZhFZQrHznU8t1DvfbtFTa+epe9+3e2ACuFo67ccVvsbZ+uRN5PKFI6da//BXD365mq1Gzxb65KNv4cAAIBrEJABAACftWjVATW9apbmLd1X7n2lZeRpwKgFGvHiHyouttihOgBwvBm/7laLa2dp+br0cu9rV0q2ut3+k8a9u1bWM+ekg9uyWq167K0k9Rr+s/5NNR4KnGtxUqqaXzNLsxfvtUN1zvHH+nQ1vXqWZi7cU+59ZWTm6/qHFum2p5aqsJBzAU/x/rfJanfjj/p7q/Ebpc71z7Yjaj9ktt79ZrMdKgMAAI5GQAYAAHzSz8tTdPmIX5WVXWDX/b47PVk3P7GUkAyA2/ty7g4NGrtYefnFdt3vs++v05iJqwjJ3JzVatXIl/7Uyx//Y9f95uQV6eoHf/OIkVS/r05Vr+E/l7rOmq0++3G7Bo1dpKIizgXc3etTN+ieF/5QUZH9jldFRVaNeOlPvfqpff+2AACA/RGQAQAAn5O08ZCufnChThTY96LwKV//tFMPvrrKIfuG/VmtVn05d4cKCm1/P+TkFuqb+TvtWBXgWL+sSNGtTy2VxeKYEOuNLzZpwidcHHZnz763Tu9OT3bIvouLrRry6BIt/uuAQ/ZvDxu2HVH/+xYoN7/IIfv//re9uvv5FQ7ZN+zjiznbNWbiXw7b/9g3kvTZj9sctn8AAFB+BGQAAMCn5OUXaejjvxsaMZE0bYD2LbhBSdMGlLnNpK8365cVKbaUCCeyWq16YtIaDX38d103ZpFNIVlObqGuuG+BBj+yRBM/2+CAKgH7OnL0hG59aqmKi8sejtlyHHxy8hqttWE9Jzjen3+n67kp6wy1MfoeKCyy6JYnl+qonUdq20NhoUU3P/m7snMKy9zGlr+Bj7/fpu9/22NDhXC0vQeyde+LfxpqY8t7YORLf3rkunwAAPgKAjIAAOBTnnl3rbbuOWqoTWxMqOKrhik2JtRQuzvGLXfLC4P4fzv3ZevNrzZKkmYv+ddwSHYqHFuSlCpJeuHD9Uo/nOeQWgF7GT3hT6VlGHuf2nIcLC626tanlpZrdCbsLy+/SLc9tUxGZ8C05T2wLy1HD73mfiOqX/povdZvMbbelK3nAnc/v8LuUziifKxWq+4Yt1zHc8sekEq2vQdy8oo07JllDhutCwAAyoeA7DwyMjI0duxY1a9fX8HBwapRo4ZGjx6tnJwcDRs2TCaTSZMnT3Z1mXCgXSnH9OIH60+vT3M8t1CHjnDRy1dYrdKGI9I7ydKp1QOskk5wjccnvDiqjaz/DNNtVzYo8fHFH1+u/NW3qmn9ik6uDOWxOyVbr03d6LT+UtJz9ArrT7i1+jUjNWfSZQoJ9pNkLCQ7NxyrEBGoX9/vo6rRIQ6tGSiPvzYc0pdznTcd6Ibtmfpgxlan9WdvxcUWzVnyr44eP/l94OjxAv20bJ9HX+x+55tkwzeKlMdHs7bp762HndbfhaQeytWLH/7ttP4OHsnXCx+sd1p/9ma1WrV0daoefTPp9Pfi3Pwijw6+Zy/5VwtXOm/6z8VJqYwkBADATRGQlWL9+vVq3ry5Xn31VaWlpSkhIUGFhYWaNGmSBg0apOTkk3O1JyYmurZQOMQ/246o34hfVL/fd3py8hrl5J2cl/7o8ULF9/pGQx9fogMHc1xcJRxpSap001LptuXSp9v//+dWSZcvOBmaOWjpIriJce+u04btR/T6Qx1UverZd4nef1NTdWsXp2feXatNOzJdVCFsMWXGFqdf1Pxo1laHrXUG+7i0YzXDIVlp4Vj75pWdUjNgq3e+2ez0Pt+dniyr0eFKLma1WjXpq02qe/m3GjBqgY7nnvw+cDy3SP1G/Kr6/b7Tex74vCwWq9771jHrjp2Po9Y6s8VHs7aqsMhy4Q3t6LPZ25VjcLSSO5j+8y41v3qWut7+kyZ88s/p78WZxwpUo9c3eva9tSpy8mtpD646DgIAAPdDQFaCjIwM9e/fX2lpaRozZoxSU1O1du1apaWlacKECZo3b56SkpJkMpnUokULV5cLO1u6OlVdbpmrn5allDjtSEGhRV/O3amON83Rjn+POb9AONy0XdJDSdLWUm6sPVpwMjS7b6XkoDW94QZOrZsRFhKgj8ddfPrnDWtX0Iv3tdXKfw7qVdYa8ij5J4r08ffOXyj94JF8zVq4x+n9whgjIRnhGDxVRma+pv+y2+n9Ju/K0u+r05zer60sFquGPbNMoyes1L+pJd8Ut3t/tu598Q+NePEPjwrJfv1jv3a5YD2kL+fudIsph4uKLJry3Ran93s0u0DT5u9yer/l8cIH63TD2MXatDOrxMcPHsnXuPfWaeDoBR51I9C2PUe14E/njR47ZdFfqUreleX0fgEAwPkRkJVg1KhRSklJ0ciRIzVx4kRFREScfmzs2LFq2bKlioqKVLt2bUVGRrqwUtjbjn+PacCohWVarHlfWo763POzjh13/Rc92M+iA9JrZZx9bc1h6Rlja5vDw6xLPqzxH/+t3hfF685rGslsNmnqi5fIZJJueXKpR0+v5ItWrDvosjVAvl+01yX9wpiyhGQWq5VwDB7r5xUpLruQ7UnTi417b60+/WH7hTeU9N63WzT+I+dN11de3y/a45J+c/OLtODP/S7p+0yrN2Vo/8Fcl/Q9y4P+BqbO3q6nJq8t07Y/LUvRPS+scHBF9vPjYtedk7mybwAAUDICsnMkJydr+vTpiomJ0fjx40vcpk2bNpKkli1bnv7ZjBkzdM0116hWrVoKDQ1V48aN9cQTT+j48eNOqRv28frUDafXFyiLnfuyNXXODgdWBGeyWqUpBpfI+C1V2ua8JRzgAs9/sE7rtxzWxDHt9fZjndSheRU98fYabXPi2h2wj9WbD7ms7zWbM1zWN4wpLSQ7NULkcNYJwjF4rNWbXHcsWpPsPmtQnU/WsROa+LmxEeIvf/KPjnvI9HmufQ+4/rNwtQs/j9dszvCI0YbFxRY9827ZwrFTPv1hu3bu84zZVVz9HgAAAO6FgOwc06ZNk8Vi0ZAhQxQeHl7iNiEhJxdePzMgmzhxovz8/PTSSy9p/vz5uueee/Tee++pT58+slg8b05uX3TseIG+mGs87PLENRVQsnVHpJ02zDjz3R67lwI3UlRk1S1PLlVwkJ/uHdREy9am6c0vyzjMEG5lzWbXXZzdlZKtzGMnXNY/jCkpJDt89OTvr6Dw5Hkd4Rg8kSsvzq5LPqziYvf/XvTZj9uVl29slF12TqG+mrfTQRXZz4mCYm3Y7rq1U90hHHBlDQeP5Gt/umtGrxnx84oU7Tlg/Ebf9791/tSVtnDle8CV56IAAKBkBGTnWLRokSSpe/fupW6TkpIi6eyAbM6cOfr22281ZMgQde3aVaNHj9bkyZO1YsUKLV++3LFFwy5+W3Xg9OLbRiTvytL2vZ5xtxzOb0mqbe1+t7EdPMfR4wWnp6T6adm+EtcnhPtzxZorZ9rt4v5hzLkh2YmC/7+wTzgGT7Vrv+uOQ7n5RTp4xDXT3Brxg41ToP3gAVPp7j+Yo8Ii14WUrv4cdocadqW4//dGW9/Lrpq+0wir1erS98Du/dlM0Q4AgJsxWRn6cpYaNWooJSVF69atU2Ji4n8eLyoqUlxcnDIyMrRz507VrVu31H1t27ZNjRo10tdff63BgweXuYacnJzTo9fi4uJkNpNjOkNOYGtlhQ+0qW3lox8qsDjFzhW5j9SoB2UxV5DZclRxWa+7uhyHqTD0dYV0ut5wO2txodLvq+OAitxH1cl7ZTL7yWopVvrIWq4u57wsClBqpSftus9FH/VV58Sq2rnvmGpVC1eLa76325fruCMvyCz3n5bJG44D6ZH3qsi/aomPJU0boNiY0FLbxsaEyN/PrKJii9Iy8s7bT1pGrtoNnv2fn8cc+0hBRfuMFe1GvOE9YIs8/wY6EnGjZPrf+ZjVqphjHyuo2HN/l7bw1d//mbzhNUiNGiuLOazEx+x1HCztGChJVbPelL/FdSOYyiI98h4V+ccabhdQlKIqxz50QEX2U2iurINRI0t87EK/f6n87wGz5Zjisl4zVrSdHYy8Q4X+NUp8zBnnAtHHpiq4yL1HGx4OH6T8wATD7UyWXFXLmuCAiuzHKrMOVHqm1MedcRysduR5mWT8xlx34A2fgwAA72GxWJSaenLUQmJiotatW2fTfvztWZQ3yMnJkSTl5ZV8sjN9+nRlZGQoIiJCdeqc/4L44sWLJUlNmjSxuZ5Tv2Q4QVS8VPKsmhd06OABKd/1i047TESxZJYsxcXav997n6ff0SMKsaGdpSDfq18XSap66l4Kq9X9n6spUKpkv93dd2OCurevpscnrdaPi/dq7fQr9clzF6vb7T/ZZf+pBw5I1rKvfegy3nAcCDlR6plPbEyo4quWfNH4TP5+5jJtV5KMg+lSnoe+dpJ3vAeMMgVKtQf/fzgmSSaTMkxtpANJktXYNGwezRd//+fyhtcgoqjUOUSccRxMT9svFbp3QKbgPJu+JRfm57j/+yKwSIoq+aGy/v4l298DlqIC179GQfkuPRc4nJEu5bj5+6TGMSnQeDNr8QnX/34vyHTe7wnOeA8c2L9PkvtPN1sib/gcBAB4pfT0dJvbEpCdIzY2VpmZmVq7dq06dep01mOpqal6+OGHJUktWrSQyWQqdT/79+/XU089pT59+pQ4Eq2sGEHmPIV+BTooSVardJ7f7blM1gLFRgfIrOoOq83VUv38ZJFk9vNTXHXvfZ6BWf/a1K74wBZV9+LXRdL//02YTG7/XC0KkL1uLahfM1LjR7fVXxsOacIn/8hisWrce2s1fnQ73Xdjgt7+enO5+4irVs0zRpB5wXHgkH+RSosi0zLOvyaI0bvGS1I1Jlz+Fs987STveA8YYVGADkcMUUHA/26IslokmU4eDyNbKbjhA6p0/FuZ5Bshma/9/kviDa9Burmg1HEL9joOnm8/cVWiZNb5Rym5Wqb5iHJlfGaAMP8sRbn5+6LYFKq0Uh670O9fKv97IMBcoCoufo0OB1hU2kSfzjgXqBwdrsAo936fZAdmy5aJIIOsGYpx878BSTpgyZPVXPJtkY4+Dpqs+apWPa7sxboZb/gcBAB4jzNHkFWtWvJsQWXBFIvnGDVqlN5++23VqFFDCxcuVMOGDSVJSUlJGjp0qHbt2qXCwkKNGDFCkydPLnEfx48fV7du3ZSWlqakpCTFxRk7ATpzisXjx48rLMy2O5NgXJdb5mrFOmOJ853XNNIHz3RxUEXuIb7nNO0/mKvqVUKVsrDs04V6muOFUt9fpTyD1zqfayVdXvJMLV6j/eyT9zmaJf01wNXVnF9ObqHCO04t935MJmnZZ1eoTUK0Wl3/g7bsPipJMptNWvllfyXUi7LLVIvHV96ssNCActfraN5wHBgzcZVen7rRprb7Ftyg+KphSknPUY1e3xhuHxEWoKwVQ2U2l/0GDHfjDe+BssrJLdQV9y3QkqSTJ9un1hzLzilU/1G/Ki//5AfFgG419d1rPRQY4OfKcp3Cl37/pfGG1+DGRxZr2vxdNrUt73GwQa1IbZtznU19O9OazRlqe8OPhtttnHW1mtav6ICK7KtGr2+Ukp5jU9vyvgduv6qhPn72Ypv6tpen31mj56est6lteZ9/YIBZ2StvdvvPjIOH8xTf6xvD69X98FZPDezu3lOxS1L3YT+d/nw3qrzvgUvaxOr3T/vZ1Lc78IbPQQCA97BXhsLQpHOMHTtW0dHR2rdvn5o2barmzZurQYMGat++verWrasePXpIklq2bFli+7y8PPXv31+7d+/Wr7/+ajgcg2vdN9jYXOsmk3TvINun0IR7CQ+QrjAYdFUKlC6t5ph64Fpjbmmui1pV1dPvrj0djkmSxWLVrU8tlb+fWZ8859qLPDCmTZMYl/Xdukm0R4djvqS0cKx988q6tGM1zZl0mUKCT17cnL3kX103ZpEKCn1jFBk8X5sE1x0HXXkMNqJNQow6taxiqE23dnEeEY5JUpuEaNf13cR1fZ+uwYV/A80bVHL7cEySqkSH6IY+pa+1XpJa1cLV72LPuGPQle9DV/79AQCAkhGQnSM+Pl7Lli1Tv379FBwcrD179qhSpUqaMmWK5s2bp23btkkqOSArLCzUtddeq9WrV2v+/PlKSDC+sC1c6/redXTnNY3KvP3rD3VQYmNOcr3JfQlSkwpl2zbQLL3STgpy/++5MKhxnQp6fkRr/fn3Qb32+X9HHG3emaVx761V17Zxuu9GjvWeokvrqkZm0LWri1vHuqZjGHK+cOwUQjJ4skvauO5YdHFr26c9cbavxndTlUrBZdq2WpVQTX3xEgdXZD+ufQ+4/rOwU4sq8vNzzcmAJ/0NvPVoRyXUiyrTtmEh/pr5+qXy9/eMy0u+/jcAAADO5hlnME7WpEkTzZ07V9nZ2crOztaqVas0fPhw5eTkaM+ePTKbzWrWrNlZbSwWi4YMGaLffvtNP/74o9q3b++i6lEeJpNJ7z3ZWQ8MbXre7fz9TJr8eCfdP7TZebeD5wn1l97tLLW/wM2lUYHSO52kRPJRr7Rl91GFtPtcnYfOkcVS8kzEL3/8j0wtPrbLOmRwjppx4epzUbzT+zWZpGFXNXR6vzCmLOHYKYRk8FRtm8YosXElp/cbGuyvGy+v5/R+bVUnPkLLP79CDWud/66phHpRWv7ZFaoRG+6kysrv5v4NFBTo/Lu7OraorOYNnf/eO1eV6BBd1cM10wAOv7axS/q1RcXIIC35+HJ1Tjz/aMq4yqFa/PHlLh2ZZ9TlF9dQtSrOXwsxNiZEV1xS0+n9AgCA8yMgM2DTpk2yWq1q0KCBQkPPPqEaMWKEvvvuOz3wwAMKDQ3VypUrT/87dOiQiyqGLfz8zHr94Y7aNudajbm52Vl3j/qZTXphZBvtW3CDRtzAqBFvFRFwMvz66CKpd3Up5H/XEPxMUkKU9FSiNLen1IpwDPA4rpgWt9/FNVS7eoTT+0XZGQnHTiEkgycymUy693rnHweH9KunqMggp/dbHg1qVdCm76/WrDcuVa9OZ8+n3eeieM2e1Ev/zLhKdeI96/geUzFY119Wx+n9utO09K6opXu7ODWpG+X0fsujcqUQLf/8Cv32YV9d26v26c+7AH+zOrWsoi9e6qpdP12nds1K/6x0R/7+Zt3lgrBy+DWNFRDAJTgAANyNv6sL8CQbNmyQVPL0ivPnz5ckvfzyy3r55ZfPeuzTTz/Vrbfe6vD6YF8NalXQxIc6aOJDHVT90q914FCeYmNC9MTwRFeXBicwmU6ODjs1QqzQIvmb5LLp2QDYR98u8UpsXEnrtxxxSn8mk/TYHSWvWwr3YEs4dsqpkKz/qF+Vl198OiT77rUeHrHODHzTkH719eJHf2vvgeNO6S8wwKwxN3vmrAv+/mZddWltXXVp7dPfB6pXCdX893q7urRyefjW5pr2804VFZU8St7e6teM1HUuCOVK061dnDonVtEf6w86rc/H7/TMcwGTyaQeHaqpR4eTIXFBYbEC/M0yefiXoruva6w3v9yozGMFTukvKiJQ9wzynBGEAAD4Em5fMeB8AdmePXtktVpL/Ec45vk8/QsAyi/ATDgGeAM/P7M+fe4S+fs75w961I1N1TnRc9Yc8UXHcgqVkp4jyVg4dsq5I8n2ph5XTl6RQ2oF7CE0xF8fj7vYaf09N6K1GtWJclp/juJN3weaN6ykJ+5IdEpfJpP06XMXKzjIfe7NNZlM+uTZixXspIWE77i6oXp2rO6UvhwtMMDPK/4WqkSH6O3HOjmtv7ce6ajYGOdP6wgAAC6MgMyA8wVkAADAMyQ2jtZTw1sZapOWkauU9BylZeSWuU39mpF6aVRbo+XByeIqh2rJx5erbdMYw+HYKadCsk4tq+i3D/uqoodNJQffc2nHarr7OmOjGWw5DrZvVlljbm5utDw4weN3tjS8Hp0t74HRQ5qqS+tYo+U5XKM6UXphZBtDbWx5/jViwzRxTAej5cEJbry8nq40uB6dLe+BKy6poaH96xstDwAAOIn73MblARYtWuTqEgAAgB08cWdLJe/K0jc/7yrT9u0Gzza0/8oVgzVv8mUKDeFUyxNUrxqmv74eUK674i/tWE09OsR5xZ318A1vPtJRO/Yd08KVB8q0vdHjYN34CH3/5qXy9+eeTHcUGOCnH9/qpS63zNW+tJwytTH6Hrj84ni98kB7W8pzigdvbqbNu7L0yffbyrS90edfMTJQ8yZfpgoRgbaUBwczmUz6/IVL1H3YT1qbfLhMbYy+BxIbV9IXL3Xl3AAAADfGtxUAAOBz/PzMmvpiV914eT277zuucqgWf3y5GtauYPd9w3HscfGKC2DwJEGBfvrhzZ66rLP9p35rUCtSiz7qq2pVwuy+b9hPzbhwLfnkctWpHmH3fffvWlMzXrtUAQHue8nBZDLpg6cv0rCrGtp935UrBmvhB33VvKGxUXpwrsjwQP06pY/aNzM+evxC2jaN0YIpfRXFqHIAANya+56tAgAAOFBAgFlfvNRVL9/fVoF2uoDXs2M1rfyyv5rWr2iX/QGAI4WFBmjO2730yO0tZDbbJ+C9tldt/TG1v2pVs3/oAvurGx+pP7/sr4Hda9plf35+Jj11V6Jmvn6pQoLdfxS1n59ZH47rojfHdji9lmR5XdImViu/6q/WCTF22R8cKzoqWL991Fd3GZx29nzuvKaRFn3UVzEVg+22TwAA4BgEZAAAwGeZzSY9cntLrfv2SnWwYe2pUyLDAzTl6Yv065Q+qhkXbscKAcCxAgP89PL97fTH1CuUUC/K5v1Urhis6a9213evXcpFYQ9TNTpE37/ZU1+O76pKFWwf7dKiYSX99dUAPTeijVuPHDuXyWTS6Jua6e/vrtLFravavJ+wEH+99UhHLf74ctWNj7RjhXC08NAAvf/URVrwQR/Vqmb7eVzNuDD98n5vffBMF0WEMbUmAACewP1v6QIAAHCwhHoV9eeX/bV0TZrenZ6sWb/tUVGR9YLtmtaL0ogbEnTTFfW4EALAo3VoUUX/zLhKv/yxX+9NT9a8ZftkvfBhUO2bVda9g5ro+t51PGLEEEpmMpk0pF99Xdm9lr75eZfe+SZZ67ZceF0ms9mk/l1r6N5BTdSzY3W7jUR0hQa1Kuj3T/vpz78P6t3pyfru190qKLRcsF2j2hV076Amurl/fabT83A9O1bX9jnX6cfFe/Xu9GQtTkotU7tu7eJ076AmurJ7LY8KhwEAAAEZAACApJMXB7u2jVPXtnE6nJWvpI0ZWrM5Q+u3HtacJf/qRKFFQYFm3X1dE7VtGqM2CTFqXKcC604B8Bp+fmZdfnENXX5xDaUfztNfGw5pzeYM/bP9iH5auu/0cfC+wU3VtmmM2jaNUb0ajJTxJmGhARp2dSPdflVD7fj3mFZvOvlZuGXPUS34c78KCi0KDvLTY8Naqk1CtNo1rawq0SGuLttuTCaTOidWVefEqnr7sU5avSlDqzdlaN2Ww8rKPqGiIquCg/xUv2ak2jSJUZuEaDWtX5FzAS8SEGDWtZfV0bWX1dG+tOOnzwc37shUTl6RrFarwkMD1LReRbVtGqN2zWJUI5bZAwAA8FQEZAAAAOeIjgpWny7x6tMlXpIU33Oa9h/MVUxUsN58pKOLqwMAx6saHaL+3Wqqf7eTa1OdeRx8dUx7F1cHRzOZTGpQq4Ia1KqgwZfXk/T/74HoCkF6+u5WLq7Q8SpGBqlXp+rq1am6q0uBi9SIDVeN2HBd3bO2q0sBAAAOwthvAAAAAAAAAAAA+BQCMgAAAAAAAAAAAPgUAjIAAAAAAAAAAAD4FAIyAAAAAAAAAAAA+BQCMgAAAAAAAAAAAPgUf1cXAACAPYWG+Ov4yptdXUaZhYbwUQwAAAAAAAA4G1flAABexWQyKSw0wNVlAAAAAAAAAHBjTLEIAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACfQkAGAAAAAAAAAAAAn0JABgAAAAAAAAAAAJ9CQAYAAAAAAAAAAACf4u/qAgDA3aQfztP85fu0ZvNhrd50SLv3H9eJgmL5+5sVExWkVo2j1SYhRl3bxqpNQoxMJpOrSwYAAIAdbdtzVAtX7tea5MNaszlDqYdyJUmph3LV6vrv1aZJjNokxOiyztVVr0aki6sFANhb5rET+mnZPq3elKE1mzO0Y1+28vKL5O9vVlRE4P+uC0SrS6tYdU6swnUBAF4nLSNXP69IOX0cPPf6aOsmMWqTEK2ubePUukm0xx4HCcgAQJLVatXytel6d3qyZi7co8IiS4nbZWTma8vuo5o2f5ckqXWTaN07qIkG962n0BAOqQAAAJ6qqMii2Uv+1bvTk/XbqgMlbmOxSuu3HNH6LUf08ffbJEm9O1fXPYOa6IpLasjPj0laAMCTrdmcoXe+2axp83cp/0RxidtkZOZrx7/H9N2vuyVJTepG6Z7rG+uWAQ0UGR7ozHIBwK6sVquWrUnTu9+evD5aVGQtcbtT10e//mmnJKlNQsz/ro/WVUiwZ10f5ewdgM87eDhP141ZpEtum6dvft5VajhWkrXJh3XHuOVqdvUs/b461YFVAgAAwFE278zURTfP1TUP/lZqOFaaX/7YrytHL9Qlt83Ttj1HHVQhAMCRjmYXaNgzy9T2hh/16Q/bSw3HSpK8K0ujXl6pxgNnas6Sfx1YJQA4TvrhPF374CJ1vf0nTf95d6nhWEnWbM7QsGeWqfk1s7TUw66PEpAB8Gk/Lt6rplfP0syFe8q1n937s9Xt9p806uU/VVBY9hNpAAAAuI7VatVrn29Qq+t/0F8bD5VrX3+sP6iW132vSV9tktVa9gsKAADXWvzXATW7epY++d/IYFulHsrVgFELdPPjvysnt9BO1QGA433/2x41vWqmZv22p1z72bkvW11v/0n3T1ipwsKyD0BwJQIyAD7rwxlbdNX9C5WRmW+3fb799WZdOXqh8vKL7LZPAAAA2J/FYtWol1fqodf+UoGdvsDnnyjW6AkrNfb1JEIyAPAAsxbuUe+7f1FKeo7d9vnF3B3qddfPyjp2wm77BABHmfLdFl3z4G86nGW/Y9ZbX23SVQ8sVP4J978+SkAGwCd9MWe7hj+3Qo64bjF/eYquf2iRx9wpAQAA4GusVqseem2VJk/b7JD9T/x8g558e41D9g0AsI+flu3ToLGLDC2zUFZ//n1QV9y3gJFkANzaZz9u093PO+b66Lyl+zTo4cVuf32UgAyAz9m4/YiGPbPcoX3MXbpPL3203qF9AAAAwDbf/bpbb3yxyaF9vPTR35q9eK9D+wAA2GZf2nENfmSxoTV2jFqxLl0Pv/6Xw/YPAOWxYdsRDX92hUP7mL3kX738yd8O7aO8CMgA+JSiIotue3qZ4TvEkqYN0L4FNyhp2oAyt3nhw/Vav+Ww0RIBAADgQAcP52nES38aamPLuaAk3fX8Ch05yhRbAOBOrFar7hy3XMeOGxvdZctnwXvfbtGiVQeMlggADlVYaNGtTy11yvXR56es1z/bjhgt0WkIyM4jIyNDY8eOVf369RUcHKwaNWpo9OjRysnJ0bBhw2QymTR58mRXlwnAgElfb9LqTRmG28XGhCq+aphiY0LL3KaoyKphzyxj/Ql4vH9Tj6ugsFjSyfVafM3R7ILTJ41Fxe49NYAjFBdbtHV31un3AMc0AJ7uwYmrDK9Ba8u5oCSlZeRprBeMHkhJyzm9TpsvngsAvi7/RNHp8+HCIovHHwe+mrdTv/yx33A7Wz8Lhj2z7PS5NAC4g7e+2qS1ycZv6rflOFhYZHHr66MEZKVYv369mjdvrldffVVpaWlKSEhQYWGhJk2apEGDBik5OVmSlJiY6NpCAZRZUZFFb37p2Kl0zrU2+bB+X53m1D4Be8g/UaSps7er45DZqtV7ug5lnrz7PTUjTzc//rtW/n3QxRU6XtLGQ7rtqaWK7fG1Dh45eSE1/XC+2t/4oz77cZvy8t1/sdnyyMjM14RP/lb9ft+p8cCZp98DaRl5euSNv7Q7JdvFFQKAcSlpOfrm511O7fOLuTt08HCeU/u0h4LCYk37aacuvmWualz2jQ79L1RMzcjTjY8s1vK1nOMC3m7nvmN6aOIqVbt02unz4YNH8tWw/3ea+NkGHc4ydrOBO7BarXpt6gan9rnnwHF9/xtT7gJwDyevj250ap+rN2Vo2Rr3PHckICtBRkaG+vfvr7S0NI0ZM0apqalau3at0tLSNGHCBM2bN09JSUkymUxq0aKFq8sFUEbzlu3TvrQcp/f77vRkp/cJlEf64TxdfOs83fLkUq3acOg/j38xd4c6DZ2jJyatdts7gMrDarXqmXfWqv2Ns/XZj9uVf+Lsuz2TNmbotqeW6aKb5yr1UK6LqnSsvzYcUsKVM/Xom6u158Dxsx6zWKVXPt2ghKtmatbCPa4pEABs9OHMrSoudu5nV0GhRZ/8sM2pfZbX4ax89bhjvm5uv1JzAAEAAElEQVR8dImWr0v/z+PT5u/SxbfO0wOvrPT4kSQASvbN/J1KuHKmXpu6UZnHCs56bOe+bD38+l9qfs33WpdsfIYWV1r5z0Gt3+L8qb64LgDAXcz5/V/tP+j8axnvfuuex0ECshKMGjVKKSkpGjlypCZOnKiIiIjTj40dO1YtW7ZUUVGRateurcjISBdWCsCIT753zYWJ7xftUeYx1p6AZzh2vECX3fVzmaYifemjv/XMu2udUJVzvfDBej03Zd0Ft1u35bB6DZ+vLC/7+964/Yguu+vn0yMFSpN/oljXP7xI85ftc1JlAFA+VqvVZUGVJwVkObmF6nvvL1pRQjB2rje/3KSHvWAKSQBn+2HRHt346JLTU6uWJvVQrnoO/1lbd2c5pzA7+PSH7S7pd+maNO3495hL+gaAM7nq+ujMhXvc8voJAdk5kpOTNX36dMXExGj8+PElbtOmTRtJUsuWLU//bNmyZerZs6fi4uIUFBSk+Pj4s6ZiBOBaVqtVK/9xzZRwRUVWrdnsWXfVwXeN//hvQ4unPj9lvTZud9/FVo3aujtLT79T9tBv084sPf/BescV5AJ3v/CHjh4vuPCGkoqLrbrtadZUAOAZDhzMVUq682cTkKTte495zFRkb3y5UUkby37u+vrUjVrlovNsAPaXl1+k259eprJOFHHk6And++Ifji3Kjlx1XUCSVm3gWAnAtaxWq1aWMFOQMxQVWW1a98zRCMjOMW3aNFksFg0ZMkTh4eElbhMSEiLp7IAsMzNTzZs316RJk/Trr79qwoQJ2rRpkzp16qSUlBSn1A6gdPvTc0/Pme4KBGTwBCcKivXRLON3Er337RYHVOMatjyXT3/wnvXI/t56uEwjBs6UfjiPNRUAeARXn4+54wWBcxUVWTTlu62G23nTuQDg66b/sus/UypeyKK/Uj1iFFluXpE278pyWf+u/hwCgH1pOcq4wGwxjuSOx0ECsnMsWrRIktS9e/dStzkVeJ0ZkA0YMEBvvPGGrrvuOnXt2lVDhgzRrFmzdPToUc2cOdOxRQO4oH9cPMLln22ZLu0fKIvZS/616UTpi7k7vGIEUVGRRZ/NNj7lSuaxAq8JiD62caqFj2YZv5gKAM62Ybtrz8eMjNB2lQV/7rdplN03P+9Sdo6xC+oA3JMtN8xJnjGVbPLuLKevQ3kmV38OAYCrz0fd8ThoslrLOmjaN9SoUUMpKSlat26dEhMT//N4UVGR4uLilJGRoZ07d6pu3bql7uvw4cOKiYnR5MmTNWLEiDLXkJOTc3r0WlxcnMxmckxXS416UBZzBZktRxWX9bqry3E6b3j+uYFNlRl+fYmPJU0boNiY0PO2j40Jkb+fWUXFFqVl5JW6XVpGrtoNnv2fnwcXbFH08WnGinYjVSfvlcnsJ6ulWOkja7m6HDhIdnAXHQvtZVPb2MyJ8rNm27ki5yo2hSqt4iM2tY3I/U2R+UvtXJHzZYTfqBOBjQy38ys+otijbzmgIrgLbzgXKC9ffw284fkfDeml4yFdSnzsQueDZT0XlEo/H4zIW6zIvCWGana240HtdTSsn01tq2RNUoDF/UfJATi/1KgxspgjDbcLLtik6OPfOqAi+znhX1sZkbeV+JgzrgsEFKWoyrEPjRUNAHaUG9hMmeHXlfiYc66PJiv6+DfGii6FxWJRamqqJCkxMVHr1l14LfmS+NulGi+Sk3Pybrm8vJJ/wdOnT1dGRoYiIiJUp06d/zxeXFwsi8WivXv36rHHHlNsbKyuv77ki/JlceqXDBeLKJbMkqW4WPv373d1Nc7nDc+/QjWp5FlTFRsTqviqYWXajb+fuczbnik//4TnvnaSqp66l8Jq9ejngQuonC2d/1yoVGnp6VKh+90JZIh/BamibU2zs48r+6AX/G3ULpACjTcrLrZwbPB23nAuUF6+/hp4w/OPPS6FlPJQGc8HbT0XlKTsYx7wWRF9TLLt6engwUPSCb6/Ah4vQjbNN5Wf5wHfecMipFKyP2dcFygsKHT/1wiAd6tQ3Suvj6anG1sq4kwEZOeIjY1VZmam1q5dq06dOp31WGpqqh5++GFJUosWLWQymf7TvmvXrlqxYoUkqX79+lq0aJEqV65scz2MIHMPqX5+skgy+/kprnp1V5fjdN7w/PMCIlTaIOK0jNwLtjdyh0RJQoL8VMlDXztJ0qnjncmk6p78PHBeOYEmZdnS0FqsuCoVZLY1XXMTVvnpgLVIMhk/PYoKNysswPP/NjIDC3XhI+J/BZjzVIVjg1fzhnOB8vL118Abnv+xkCCVNtb5QueDRkeQlSQyIlgRbv5ZkRdgLvWc+bysVsVWDpOf1b2fH4ALO2jKUWFpKdJ5hAUVKcrNPx8K/CroUCmPOeO6QGCAVNnNXyMA3s2bro+eOYKsatWqNu+HKRbPMWrUKL399tuqUaOGFi5cqIYNG0qSkpKSNHToUO3atUuFhYUaMWKEJk+e/J/2W7duVVZWlnbv3q1XX31VBw8e1IoVK1SzZs0y13DmFIvHjx9XWJiNt/DBbuJ7TtP+g7mqXiVUKQsHu7ocp/OG579tz1E1GjDD5vb7Ftyg+KphSknPUY1exocCP31XKz07orXN/bta+9k6eVFM0l8DXF0NHCXz2AlV7zlNefnG1hO7vncdTX+1h4Oqcq6bHluir+btNNQmKNBPKQtuUEzFYAdV5Ty/rTygnsPnG2739mOdNHJwggMqgrvwhnOB8vL118Abnv8383dq8CNLbGpb3nNBSfrhrZ4a2N29p6rOyS1U9V7f6Gi2sfXELr84XvPe6e2gqgA406uf/qOxbyQZbrdi6hXqnGj7BUpnyDp2QhW7fGlz+/J+Ftx1XWO9/9RFNvcPAOW1ZXeWmgycaXP78h4Hx93TSs/cY5/ro/bKUBiadI6xY8cqOjpa+/btU9OmTdW8eXM1aNBA7du3V926ddWjx8kLgC1btiyxfaNGjdShQwfdcMMN+u2335Sdna1XXnnFmU8BQAnq14xURFiAy/pvkxDtsr6BsqoYGaQhl9cz3O7eQU0cUI1r2PJcbuhT1yvCMUnq0SFOjWpXMNQmLMRfN/ev76CKAMB+2iTEuLb/Jq7tvyzCQgN064AGhtuNuIGbJABvcftVDRUU6GeoTctGldSpZRUHVWQ/UZFBqlcjwmX9t2nCdQEArtWwVgWFh7ry+qj7nQ8TkJ0jPj5ey5YtU79+/RQcHKw9e/aoUqVKmjJliubNm6dt27ZJKj0gO1NUVJTq16+vHTt2OLpsABdgNpvU1oUH4bZN3e8DACjJk8MTVaVS2cOea3rW1iVtYh1YkXN1allFg/vWLfP2MRWD9fTdiY4ryMlMJpPeHNtRfn7/nUa6NC+NaqvIcBsWLgMAJ6tXI1JREa45XlWNDlH1qp4xFfHY25obWlPi8ovj1bszU4YB3iI6KljP3tuqzNv7+5n0+kMdSlyGxB21a2r7MijlxXUBAK5mNptcehM/AZmHaNKkiebOnavs7GxlZ2dr1apVGj58uHJycrRnzx6ZzWY1a9bsgvs5ePCgtm7dqnr1jN+ND8D+brRhZIw9dGsXp2pVmCoVnqFWtQj98n4fxcaEXHDbKy6poS9e6uoxX4bLwmQy6dPnL9GVPS48BVaVSsH6+b3eqhtvfI0Gd9anS7ymvtBVAf4XPk189t7WGjWkqROqAoDyM5tNhm6CsKch/ep5zOdltSph+uX93mUKyXp2rKbpr/aQnx+XFgBvMva2Fnps2IVvDA8MMGvahO7q0aGaE6qyD1ddF2hSN0qJjRlBBsD1XHUc7NE+TnGV3e+GMc5iDdi0aZOsVqsaNGig0NCzf5k33XSTxo0bpx9++EFLlizRhx9+qG7dusnf318PPPCAiyoGcKbBfeuqggvuGvam6efgGxIbRyvp64F6YGjTEu+0b96got57srO+f7OnQoL9XVChYwUF+mnGaz30wdMXqUXDSv95vEJEoEYPaaqkaQPd8u4ne7ixXz398cUVGtSnjvxLGE12Wefq+umdy/T03WW/uxgA3ME917vmvOzu6xq7pF9bJdSrqKRpAzT2tuaKjgr6z+ON61TQpEc76qd3ert0mh4AjmEymfTS6LaaPamXLi0h/AoMMGtIv3pa+WV/XXtZHRdUaLvLL45XzTjn38B676AmHnOjBADvduPl9RQZ7vzzN3e9Pup9V7UcaMOGDZJKnl6xY8eOmjp1qt566y3l5+erRo0a6t69ux5//HHVquXeCzEDviIsNEB3XNVQr03d6LQ+46uG6Uo3X4wdKEl8bJhef7ijXhjZVr/+maJDR/IVGOCnhrUj1bFFFa//cufnZ9ad1zbWHdc00l8bDmnL7qM6UVisyhWDdVmn6grzgYuBbZtW1jev9FBaRq4W/5WqYzmFCg/1V/tmldWglrF1ygDAXTRvWEnd28VpcVKq0/rs2yXeI4+bsTGhmvBAez17b2v9+sd+pR/OU0CAWfXiI9WldVWvPxcAIPXvVlP9u9XUlt1ZWr0pQzl5RaoQHqAe7aupSvSFZ5xwR35+Zo0YlKBH3kxyWp8VIgI19ArW7AXgHsJDAzTsqoZ644tNTuuzZlyYBnRzz+ujBGQGnC8gGzlypEaOHOnskgAY9PTdrfTtr7u1Ly3HKf29/1RnBQQwWBeeKzTEX1f2qO3qMlzGZDKpQ4sq6tDC/Rcdd5TYmFANdtEUDADgCJMf76RW1/+ggkKLw/sKCfbTpEc7ObwfRwoO8tcAbvgCfFrjOlFqXCfK1WXYzeibmuqLuTu0cUemU/p7/aEOLpnNBgBK88zdrfXdr3uUku6k66NPXuS210fdsyo3db6ADIBniAwP1Efjuhhul5aRq5T0HKVl5Ja5zS0DGqjfJTUN9wUAAADHSahXUc+NaG2ojS3ngpL08uh2ql/Tu9aqBABPFxTop89euER+JUwlfj62fBb07RKv265sYLREAHCoChHOuz5625UN1PfiGob7chZGkBmwaNEiV5cAwA4u6xyvR4e10Msf/1PmNu0GzzbUR7P6FfXm2A5GSwMAAIATjLm5uX5fnab5y1PKtL3Rc0FJurJHLY0cnGC4HQDA8dokxGjig+31wKurytzG6GdBrWrh+mhcF6akBeCWel8Ur7G3Ndcrn24ocxujx8HmDSrqjYc7Gi3NqRhBBsAnvTSqrUbc4JjFIRvXqaBfp/RRVOR/FzQHAACA6/n7mzXjtUvVo32cQ/bfu3N1TZvQTWYzF0UBwF3dP7SZnr3X2IjisoqvGqYFU/qoWpUwh+wfAOzh5fvb6e7rGjtk303qRumX9/u4/RSzBGQAfJLJZNLbj3XSs/e2tuuFi4tbV9XST/sprnKo3fYJAAAA+wsN8de8dy7TjXZeZ/HWgQ00++1eCg5iwhYAcHdP391Kbz/WSYF2XBsnsXElLf+8nxrUqmC3fQKAI5hMJr37ZGc9c3cru14f7do21mOujxKQAfBZJpNJT9/dSn9+0V9N6kaVa18hwX56c2wHLfmknypXCrFPgQAAAHCo4CB/ffVyN814rYcqVwwu175iY0L041s99enzlygwwM9OFQIAHG3k4AStnX6l2jWLKdd+AvzNem5Ea/311UDVqhZhp+oAwLFMJpPG3dtaf0y9Qo3rlC/YDw3216RHO2rRR5crppzn1s7CLW0AfF775pW1dvpAfTBjq96dnqyte46WuW1EWIBu7l9fDwxtpno1WIAdAADAE13Tq466to3TpK836YMZW5V+OK/MbatVCdXwaxrpvhubqlIFptgGAE/UtH5F/TG1vz6fvV2Tv9ms9VuOlLltcJCfbry8nh4c2kxN61d0YJUA4DgdWlTRum+v1JTvtui9b7cYvj56y4AGenBoM9WJ96wbBAjIAEAn7x4eNaSp7rsxQYv/StWPi/dqTfJhrUs+rNz8orO2bVI3Sm0SonVJ61jd0LeuIsLcey5dAAAAXFhMxWA9N6KNnhyeqB8W7dWCP/drzebD2rgjU4VFltPbBfib1aJhJbVJiNZlnaprQLdaCrDj1FwAANfw9zdr2NWNdPtVDbXqn0P6bsFurdmcobXJh5WdU3jWtvVrRqpNQrQuSqyqIf3qc4MEAK8QHOSv0Tc106ghTbVoVap+XLJXazZnaP2WI6VeH+3aJk439K2r8NAAF1VdPgRkAHAGk8mkHh2qqUeHapKk4mKLDmXmK/G675V+OF9xlUO0+YdrXFwlAAAAHCUwwE/X966r63vXlSSdKCjWkaMndKKgWEGBfoqOCmIKRQDwYiaTSR1bVlHHllUkSRaLVYcy89Xy2lknrwvEhGj73OtcXCUAOI7JZNKlHavp0o7ef32UgAwAzsPPz6zYmFD5+528K9hsst+ClQAAAHB/QYF+HrHAOADAMcxmk6pGh/z/dQEz1wUA+BZvvj7KPBAAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAAAAAAAAAADwKQRkAAAAAAAAAAAA8CkEZAAAAAAAAAAAAPApBGQAAACAj/to5lZ9M3+nze2Liy26f8JKbd6ZaceqAAAAAABwHH9XFwAAAADAdT6auVV3PrtcZrNJknRD33qG2hcXW3TbU8v0xdwd+ubnXVr0UV8l1KvoiFIBAAAAALAbRpABAAAAPmxtcoYkyWKxashjvxsaSXZmOCZJh4/ma1dKtkPqBAAAAADAnhhBVoqMjAy98sormjVrllJSUlS5cmVdffXVeumllzRq1Ch98sknevvttzVy5EhXlwoAAADYbPLjnWWxSlO+23I6JJMuPJLs3HDM39+kGa9dqiu61nR4zc6SeeyEPvtxu35YtFfph/NO/+yvDYfUvnllF1fneBaLVb/+sV8fzdqqg0dOPv+MrHxNnb1d1/euo+Agvk4CALzb0ewCfTF3h2Ys2H36XODIsRNasS5dnROryGQyubhCx7Jarfpt1QF9OHOrtu89pqJii6pUCtHgvnU1uG89hYZ4/7nAv6nHNeW7LVr01wEdyylUeEiALm5dVfcMaqJ6NSJdXZ7D5Z8o0ne/7taX83Yq9VCu/PxMqls9UsOubqg+F8WfnoXCmyVtPKT3vk3WP9sylX+iSJUqBOnKHrV068CGqlQhyNXlOVxGZr4++X6b5vz+71nfidZvOazExtEurq78TFar1erqItzN+vXr1bdvX6WlpSksLEwNGzbUgQMHlJ6ern79+unIkSP6888/tWzZMnXp0sXu/efk5Cg8PFySdPz4cYWFhdm9DxgT33Oa9h/MVfUqoUpZONjV5Tidrz9/ideg/WzJopPDjv8a4OpqAAD2ZrFYde+Lf2jKd1skSWazSV+N73o6JDv3c7C0cGxg91ouew72ZLFYNe69tZr4+Qbl5ReXuE37ZpX19YRuXnthZPnaNN361FLt3FfyiMDoqCC9+mB73XZlQydXBgCA41mtVo3/6G+99NHfyskrKnGbxMaV9PXL3dWkbpRzi3OSpI2HNPTx37V1z9ESH4+KCNSL97XRvTckOLky58jJLdRdz6/QtPm7ZLGUfPn86ktr65PnLlaFiEAnV+ccU2dv15jX/lJGZn6Jj9eNj9Cnz12sS9rGObky59iVckw3PrJEqzYcKvHx4CA/PTi0mZ4f2cYrg8KiIosefTNJk79J1omCkr8TXdy6qr56uZtqxIY7uTr7ZShMsXiOjIwM9e/fX2lpaRozZoxSU1O1du1apaWlacKECZo3b56SkpJkMpnUokULV5cLAAAAlJvZbNK7T3TWXdc1lnT+6Ra9PRyzWq26Y9wyPT9lfanhmCT9tfGQOt00R1t3ZzmvOCdZ8Od+9Rw+v9RwTJIOZ53Q7U8v0+tTNzixMgAAHM9qtWrUyyv1xNtrSg3HJGn9liO66OY5+mfbESdW5xzL16ap2+0/lRqOSVJWdoFGvPSnnp+yzomVOUdObqF63fWzvpq3s9RwTJJm/bZHXW+fp6PZBU6szjne+nKjbnlyaanhmCTtSslWr7t+1i8rUpxYmXNs33tUnW6aU2o4Jkn5J4r10kd/67anlsrbxiAVF1s05LElem3qxlLDMUlatjZdnW6ao70HPHeafQKyc4waNUopKSkaOXKkJk6cqIiIiNOPjR07Vi1btlRRUZFq166tyEjvvFsUAAAAvqcsIZnVavXqcEySJk/brE9/2F6mbQ9l5uuK+xaosNDi4Kqc58DBHF3z4G86UVC25zRm4l9atOqAg6sCAMB5Pv1huyZP21ymbTOPFajfiF+Vl196kOZpDmfla8CoBcot43N6+p21mrPkXwdX5Vwjx/+pP/8+WKZt/956RLc9vdTBFTnX76tT9cCrq8q0bUGhRdeO+U0paTkOrsp5ioosumLkAh08Uno4eKapc3bozS83Obgq55rwyT/69pfdZdp2/8FcDRy98LxhsjsjIDtDcnKypk+frpiYGI0fP77Ebdq0aSNJatmyZan76du3r0wmk8aNG+eIMgEAAACHKC0kO3WBJPN/63BI3hmOFRdb9NrUjYba7Pj3mGYv2eugipzvgxlblZ1TaKjNa4wiAwB4CavVqomfG/tcS0nPKfOFZE/wyffblHnM2IgobzoXSD2Uqy//d75bVt//tlc7/j3moIqc77XPN8rIgKjjuUWaMmOL4wpysrlL/9W2vaWPnizJG19sVHGxd9w0d6KgWG99ZSzw+3vrES1cud9BFTkWAdkZpk2bJovFoiFDhpyev/JcISEhkkoPyL799lutX7/eUSUCAAAADlVSSHbqIsmpKQe9MRyTpPnLU7T3wHHD7d75JtkB1ThfYaFFH8zcarjd/OUp2pXiPReFAAC+6/fVaUrelWW43TvTyzbizN1ZLFa9963xoOP31WnatCPTARU530eztqqo2PhImPe+9Y7zwb0HsjV3qfERgR/O3KqCwtKn4vMk7043/rvcl5ajuUv3OaAa55u1cE+ZR8+dyVO/E/m7ugB3smjRIklS9+7dS90mJeXknKolBWTHjh3T/fffr4kTJ+qmm26yS00NGjSQ2UyO6WqpUQ9K5gpKTUtVfHy8q8txOl9//hKvQdXJe2Uy+6nYUqz4eO+6GAoAKJlVJoWG9lNucLtzHihWZOZ0jRj6tEa4pjSHORrSSwrpYrjd4r9SVD0+Xp6+NHehOVoHo0YZbme1Sm0vuUGhBf84oCoAAJznWHBXKbSH4XZJGzNUPb6WTPLsgKDYFKm0imNsatul920KO7HazhU5X0bEzVJAPcPt3v7kZ01/8xoHVORcuYHNZA2/znC79MN5qlGvrQIspa/Z5Sn2V3xSMgUYbnfT8GdVIe9XB1TkXFmh/aTg9obbzVm0RfHxtzmgopJZLP8/Yq9Lly5at8629RAJyM6wd+/JqVFq1Sr54m9RUZFWrFghqeSA7IknnlDDhg01ZMgQuwVkqampdtkPyimiWDJLluJi7d/vmcNFy8XXn7/k869B1VNj661Wn3z+AOC7PpQa1pCCYk/+p9UqZSzUkbRFri3LUaoVSCE2tDP56cCBdMnq4euPBPtLUbY1zTyap8wjnCMAADxc7Akp1LamB9IOS8Uevg5TULFU0bamWcfylJXhBecC9SQZz0ZUWGT2juslFetKJU+sdkEHM45KeZ7+GpilSja8AXRyqsnjBzz9+UuKL5KCjTezKtBlfwPp6ek2tyUgO0NOzskPsby8vBIfnz59ujIyMhQREaE6deqc9djq1av14Ycfas2aNXatKS4ujhFkbiDVz08WSWY/P8VVr+7qcpzO15+/xGsgk+n0/1b3xecPAD7IKpMyw65U3qlwTDr5eRDTSxVDjim0wNhaXZ7gaEigjE+wKMlapGrVqnrBCLIKKtty9P9VsUKIQkM4RwAAeLZjwUHKtrFttdhomWy908RNFJsilSadvCnKZOzMJioyRGFBnn8ukBFg1Qkb2gX4W1TFC66X5AaGytbJMqvEVFCAxfNfg/3WQptGkIWH+quCF7wHskL8ZUvUb1KBqjnx+VssltODi6pWrWrzfgjIzhAbG6vMzEytXbtWnTp1Ouux1NRUPfzww5KkFi1ayHTGh0RxcbHuuusujRw5Uk2bNrVrTdu3b1dYWJhd9wnj4ntO0/6DuYqLjVPKxhRXl+N0vv78JV6D9rMliyQ/s9/pqWYBAN6ruNii255api/+t0C5n5/Us0N1/fLHfslk1tHI6/Xu+Hd0Q1/j08+4s19WpKjPPb8Ybtf7olr6+X3P/3wsKrKo7uXfal+asa/EZrNJ61d8q5pxNt5uDACAm1ixLl1dbplruN1Frapq+ed7HVCRc1ksVjUZOFPb9h413Hblws/UqE6U/Ytyspc//luPvWV8qsgxd12u8aOfcUBFzpWSlqPafaer2OA6bNWqhGrv2jXy9/f8gR79Rvyin5YZP7f/9pNn1ffiTxxQkXPN+HW3rnvI+Iwh1/RO0HevOe87UU5OjsLDT37/WL58uc378fx3rB317NlTkjRhwgRt27bt9M+TkpLUvXt3ZWRkSJISExPPajd58mSlp6dr3LhxzioVAAAAcIhzwzF/f5Nmvt5TP73bW3dd11jSyYsnQx77Xd/M3+nKUu2uV6fqqlcjwnC7ewc1cUA1zufvb9Zd1zY23K5/1xqEYwAAr9A5sYpaNKxkuN2913vHuYDZbNI91xs/F+jZsZpXhGOSdPuVDRUYYOySuckkm86h3FF8bJgGdjO+9vxd1zb2inBMsu3cvk71CPW+KN4B1TjfwO61FFfZ+FyznvqdyDvetXYyduxYRUdHa9++fWratKmaN2+uBg0aqH379qpbt6569Di5SOeZ649lZGToqaee0tNPP62ioiJlZWUpKytLkpSfn6+srKyzFowDAAAA3FVJ4diM1y7VwO61ZDab9O4Tnb06JDObTRp7WwtDbZrUjVK/S2o4qCLnu+OaRqoYGVjm7c1mk8bc3NyBFQEA4Dwmk0ljbzP2uVY3PkLX9KrtmIJc4JaBDVSlkrEFiB66xXvOBapEh+i2KxsaajOod13Vrm78Jit3NeaWZjKbyz7FZlREoO68ppEDK3KuPhfFq1l9Y4vxPWTwNXNnAQFmjbm5maE27ZrFqFu7OAdV5FgEZGeIj4/XsmXL1K9fPwUHB2vPnj2qVKmSpkyZonnz5p0eVXZmQJaSkqLs7Gzdddddqlix4ul/0smRaBUrVtS///7rkucDAAAAlNX5wrFTfCEku/OaRho5OKFM21arEqp5ky+Tn5/3fK2qGh2iH9/qpdDgss3G/87jnXRxm9gLbwgAgIcY0q++Hrm9bDfMVKkUrJ/euUxBgX4Orsp5KkYGae7kyxQZXrY1mF57qL3XjJw55c2xHdS9jBf7OzSvrA/HdXFwRc7VObGq3n+yc5m2DQny0w9v9bRpxJG78vMza+7kXoqvWrZlj+65vrHu8dDRU6V58OZmunVggzJtW6d6hH54s+dZS1J5Eu/5JmcnTZo00dy5c5Wdna3s7GytWrVKw4cPV05Ojvbs2SOz2axmzf4/Qa1fv74WL178n3+SdMstt2jx4sWKjeULIwAAANxXWcKxU7w9JDOZTJr0aEe9fH9bVYgofSRVj/ZxWvllf9WJ9567hU+5uE2sfv/08vPeOVutSqi+eaW77vaSKaUAADjT+NFt9ebYDucdVd2lVVX9+WV/r5la8EztmlXW8s+uUOsm0aVuUzU6RJ89f4ke9MKR5MFB/vrp3cs0/NpGCihl2kA/P5OGXlFfv33YV+GhZQsTPcmd1zbWdxN7nDckalovSr9/2k9d23rmyKHzqVUtQn9+0V+XdqhW6jaR4QF6aVRbvfNEZ48Nh0pjMpn08bMX65m7Wyk8tPQb5/p2idefX/ZXtSplCxPdkclqtRpbcc9HrVq1Sh07dlSjRo20ZcuWC25vMpn0zDPP2LQu2ZkLzB0/flxhYZ77BvMW8T2naf/BXFWvEqqUhYNdXY7T+frzl3gN2s+WLDp5V8VfA1xdDQDAnoyEY2eyWKy698U/NOW7k+fGZrNJX43vqhv61nN4zc6Sk1uoafN36ftFe3Tk6AkFB/mreYOKuuvaxmpqcNoVT2S1WrViXbo+mrVNu1KOqdhiVWx0qIb0q6cB3Wp6zToTAACUJi+/SN/+slszFuxWRla+ggL9lFA3Sndd11gtG5UeHnkLq9WqvzYc0gczt+rLuTtUUGhRcKCfPn/xEl3Zo5YCA7xn5FxpDh7O08ffb9Oivw7o99VpKiyyKCLUX1tmX+vRoUBZFRVZNHfpv/py7k7N+f1fFRRaFBLsp1/e66Murat6XTBUks07MzXluy16/7stKii0KCjQrLcf7aTBl9fzynD0XMeOF+jLuTs05/d9yso+oZAgfyU2rqS7r2uihrUruKwue2UoZZs3A9qwYYOks6dXBAAAADzdHeOWGw7HpP8fSSZJU77bcnokWWCAn67uWduRJTtNWGiA7rimke7wojUVjDCZTOrSOlZdWjMjBgDAN4UE++uWgQ10SxmnGvM2JpNJHVpUUYcWVfTLihTtP5ir6KggXd+7rqtLc5oq0SF67I6WeuyOlqdvno4MD/SJcEyS/P3NurJHbV3Zo/bp518pMsinpthOqFdRbz3aSTMX7tH+g7mKiQrWndc2dnVZThMZHqh7b0jQvTeUbRp6T0NAVkZGAzIG5gEAAMAT9OxYTVPn7JDZrDKHY6ecG5JVqxyqFg0rOapUAAAAAADshoCsjBhBBgAAAG80pF99SVJ4aIChcOyUUyFZdIUg3XZlQ9WvGWnvEgEAAAAAsDsCsjJatGiRq0sAAAAAHOJUSGYrs9mkF0e1tVM1AAAAAAA4HqsqAwAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQAQAAAAAAAAAAwKcQkAEAAAAAAAAAAMCnEJABAAAAAAAAAADApxCQnUdGRobGjh2r+vXrKzg4WDVq1NDo0aOVk5OjYcOGyWQyafLkya4uEwAAAAAAAAAAAAb4u7oAd7V+/Xr17dtXaWlpCgsLU0JCgg4cOKBJkyZp586dOnLkiCQpMTHRtYUCAAAAAAAAAADAEEaQlSAjI0P9+/dXWlqaxowZo9TUVK1du1ZpaWmaMGGC5s2bp6SkJJlMJrVo0cLV5QIAAAAAAAAAAMAAArISjBo1SikpKRo5cqQmTpyoiIiI04+NHTtWLVu2VFFRkWrXrq3IyEgXVgoAAAAAAAAAAACjCMjOkZycrOnTpysmJkbjx48vcZs2bdpIklq2bHn6Z0uWLJHJZPrPP6ZgBAAAAAAAAAAAcC+sQXaOadOmyWKxaMiQIQoPDy9xm5CQEElnB2SnvPPOO2rduvXp/w4LC3NMoQAAAAAAAAAAALAJAdk5Fi1aJEnq3r17qdukpKRIKjkgS0hIUMeOHR1THAAAAAAAAAAAAMqNgOwce/fulSTVqlWrxMeLioq0YsUKSSUHZPbWoEEDmc3MhOlqqVEPSuYKSk1LVXx8vKvLcTpff/4Sr0HVyXtlMvup2FKs+PiSj48AAAAAAMA7+fp1EYnXgOfv28/f3VgsltP/v0uXLlq3bp1N+yEgO0dOTo4kKS8vr8THp0+froyMDEVERKhOnTr/eXzQoEHKyMhQdHS0BgwYoJdfflkxMTE215OammpzW9hRRLFklizFxdq/f7+rq3E+X3/+ks+/BlWt1pP/x2r1yecPAAAAAIBP8/HrIpJ4DXj+vv383Vh6errNbQnIzhEbG6vMzEytXbtWnTp1Ouux1NRUPfzww5KkFi1ayGQynX6sQoUKevjhh3XJJZcoPDxcf/75p8aPH6+VK1dq9erVCg4OtqmeuLg4RpC5gVQ/P1kkmf38FFe9uqvLcTpff/4Sr4FOHe9MJlX3xecPAAAAAIAP8/nrIuI14Pn79vN3NxaL5fTgoqpVq9q8HwKyc/Ts2VPJycmaMGGCevXqpYYNG0qSkpKSNHToUGVkZEiSEhMTz2rXqlUrtWrV6vR/d+vWTc2aNdOAAQM0bdo03XbbbTbVs337doWFhdn2ZGA38T2naf/BXMXFxillY4qry3E6X3/+Eq9B+9mSRZKf2e/0OowAAAAAAMA3+Pp1EYnXgOfv28/f3eTk5Cg8PFyStHz5cpv3w9Ckc4wdO1bR0dHat2+fmjZtqubNm6tBgwZq37696tatqx49ekgq2/pjV1xxhcLCwrR69WpHlw0AAAAAAAAAAIAyIiA7R3x8vJYtW6Z+/fopODhYe/bsUaVKlTRlyhTNmzdP27Ztk1S2gOyUM6diBAAAAAAAAAAAgGsxxWIJmjRporlz5/7n58ePH9eePXtkNpvVrFmzC+5n9uzZysnJUfv27R1RJgAAAAAAAAAAAGxAQGbApk2bZLVa1bBhQ4WGhp712E033aS6deuqdevWCg8P159//qlXXnlFiYmJuuGGG1xUMQAAAAAAAAAAAM5FQGbAhg0bJJU8vWLTpk319ddf680331ReXp7i4+N155136plnnlFgYKCzSwUAAAAAAAAAAEApCMgMOF9A9thjj+mxxx5zdkkAAAAAAAAAAAAwyOzqAjzJ+QIyAAAAAAAAAAAAeAZGkBmwaNEiV5cAAAAAAAAAAACAcmIEGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoBGQAAAAAAAAAAAHwKARkAAAAAAAAAAAB8CgEZAAAAAAAAAAAAfAoB2XlkZGRo7Nixql+/voKDg1WjRg2NHj1aOTk5GjZsmEwmkyZPnuzqMgEAAAAAAAAAAGCAv6sLcFfr169X3759lZaWprCwMCUkJOjAgQOaNGmSdu7cqSNHjkiSEhMTXVsoAAAAAAAAAAAADGEEWQkyMjLUv39/paWlacyYMUpNTdXatWuVlpamCRMmaN68eUpKSpLJZFKLFi1cXS4AAAAAAAAAAAAMICArwahRo5SSkqKRI0dq4sSJioiIOP3Y2LFj1bJlSxUVFal27dqKjIx0YaUAAAAAAAAAAAAwioDsHMnJyZo+fbpiYmI0fvz4Erdp06aNJKlly5b/eez7779X586dFRYWpgoVKuiiiy7Spk2bHFozAAAAAAAAAAAAyo6A7BzTpk2TxWLRkCFDFB4eXuI2ISEhkv4bkE2aNEnXX3+9unTpotmzZ2vatGnq2bOn8vLyHF43AAAAAAAAAAAAysbf1QW4m0WLFkmSunfvXuo2KSkpks4OyHbu3KmHH35Yb7zxhkaOHHn655dffrmDKgUAAAAAAAAAAIAtCMjOsXfvXklSrVq1Sny8qKhIK1askHR2QPbJJ58oICBAd955p13radCggcxmBvq52v+xd+9xXtV1/sBfc+F+UbnIIIMiAgkoUAJJmYmBaV6yzNI1s9Z2u7na5kptbdt20yzbdl27WNvVLaK0XJOsVLK8dAEBMyBFFOIyo47gheE6l98f/mRFBpgZhvkC5/l8PHjA95zz+Xze54Df4+O85vM5NQd/KCk/KDW1Namuri51OZ2u6OefuAaDrluRsvKKNDY1prq65e9HAAAA4MBU9OciiWvg/It9/vuapqambX8+4YQTsmDBgnb1IyB7ifr6+iTZ6bKIs2bNSl1dXfr06ZMjjzxy2/b77rsvL3vZy/I///M/+cxnPpOVK1dm5MiR+dd//decf/757a6npqam3W3pQH0ak/KkqbExq1evLnU1na/o558U/hoMam5+/g/NzYU8fwAAACi0gj8XSeIaOP9in/8+7PHHH293WwHZS1RVVWXdunWZP39+pkyZst2+mpqaXHHFFUmScePGpaysbLt9q1evzj//8z/n6quvztChQ/PNb34zf/M3f5OBAwdm2rRp7apn8ODBZpDtA2oqKtKUpLyiIoOHDCl1OZ2u6OefuAZ54fuurCxDinj+AAAAUGCFfy4S18D5F/v89zVNTU3bJhcNGjSo3f0IyF5i2rRpWbJkSa6++upMnz49o0aNSpLMnTs3F154Yerq6pIkEyZM2K5dU1NT1q9fnxtuuCFnn312kuR1r3tdFi9enE9/+tPtDsiWLl2aXr16tft86BjV02Zm9RMbMrhqcFb9eVWpy+l0RT//xDWYfEvSlKSivGLbexgBAACAYij6c5HENXD+xT7/fU19fX169+6dJLnnnnva3Y+pSS8xY8aM9O/fPytXrszYsWNz7LHHZuTIkZk8eXKGDx+ek08+Ocn27x9Lkn79+iXJdkFYWVlZpk2blj//+c+ddwIAAAAAAADskoDsJaqrq3P33Xfn9NNPT/fu3bN8+fL069cv119/fWbPnp2HH344yY4B2dixY3fa56ZNm/ZqzQAAAAAAALSeJRZbMHr06Nx66607bF+/fn2WL1+e8vLyHHPMMdvte+Mb35hvfetb+dWvfpU3v/nNSZ5fdvH222/PpEmTOqVuAAAAAAAAdk9A1gaLFi1Kc3NzRo0alZ49e26378wzz8xrXvOa/P3f/32eeuqpHH744fnv//7vLFq0KLfffnuJKgYAAAAAAOClBGRt8OCDDybZcXnF5Pn3jd1yyy358Ic/nI9+9KN59tlnM378+Pz85z/f9t4yAAAAAAAASk9A1ga7CsiS5OCDD87111+f66+/vjPLAgAAAAAAoA3KS13A/mR3ARkAAAAAAAD7PjPI2mDOnDmlLgEAAAAAAIA9ZAYZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAIAXaW5uzmOrnstP7lie+o0NSZL6jQ35zbyaPLt+S4mroyNUlroAAAAAAACAUmtsbMov7l2Vb9z0UH57f23WPbt9EPb0c1ty0t/+PEky6oiD8qbXHZH3vOXoHFndpxTlsocEZAAAAAAAQGE1NTXnv3/yUK767weyfM36VrV5eMUzufpbf8rnv/2nvOE1Q/OFD03O6OEH791C6VCWWAQAAAAAAApp+ernMv3vb8t7PnVvq8OxF2tuTmb/dmVe/tab8/lv/SmNjU17oUr2BgEZAAAAAABQOHf8fnWOPeenmfPHmj3ua/OWxnz4P+Zm+t//wjvK9hMCMgAAAAAAoFB+cc+qnP6BX2X9hq0d2u+v59bklPf8Is/VC8n2dQIyAAAAAACgMOYvrsubP3RHtmzdO8sh/uHBJ3POh+5MU1PzXumfjiEgAwAAAAAACmHzlsa842O/ycZNja1uM3fmWVl5+3mZO/OsVre5/Xdr8uUfLm5PiXQSARkAAAAAAFAIn75+QRYte7pNbaoG9Ez1oF6pGtCzTe0+8h/zsmzls21qQ+cRkO1CXV1dZsyYkREjRqR79+4ZOnRoLrvsstTX1+fiiy9OWVlZrrvuulKXCQAAAAAA7EZt3YZ8/tsPdtp4GzY15OPX3d9p49E2laUuYF+1cOHCnHbaaamtrU2vXr0yZsyYrFmzJtdee22WLVuWtWvXJkkmTJhQ2kIBAAAAAIDd+u+fPJStDXvnvWM7c+Pty/OlGRszqH+PTh2X3TODrAV1dXU588wzU1tbm8svvzw1NTWZP39+amtrc/XVV2f27NmZO3duysrKMm7cuFKXCwAAAAAA7EJDQ1O+fuNDnT7u1oamfPMnnT8uuycga8Gll16aVatW5ZJLLsk111yTPn36bNs3Y8aMjB8/Pg0NDRk2bFj69u1bwkoBAAAAAIDdeXDp2qysrS/J2LPvXlmScdk1AdlLLFmyJLNmzcqAAQNy1VVXtXjMcccdlyQZP378tm0nnXRSysrKWvz13ve+t1NqBwAAAAAAdjRvUV3Jxl7wl6fS0MlLO7J73kH2EjNnzkxTU1MuuOCC9O7du8VjevR4fq3QFwdkX/nKV/Lss89ud9zs2bPzmc98JmecccbeKxgAAAAAANil+5c8VbKxN25qzF8eezrHjOxXshrYkYDsJebMmZMkmTp16k6PWbVqVZLtA7IxY8bscNxnP/vZDBw4MKeeemoHVwkAAAAAALTWX2vWl3b82noB2T5GQPYSK1asSJIcccQRLe5vaGjIvffem2T7gOylnnzyyfziF7/I+9///lRWtv8yjxw5MuXlVsIstZqDP5SUH5Sa2ppUV1eXupxOV/TzT1yDQdetSFl5RRqbGlNd3fL3IwAAAHBgKvpzkcQ1OBDOv67PRUmX4S3umzvzrFQN6LnTtlUDemz7feXt5+1ynNq6DZl0/i07bL/wHX+bHluXtKFidqap6f+WqzzhhBOyYMGCdvUjIHuJ+vrnX9K3cePGFvfPmjUrdXV16dOnT4488sid9jNz5sw0NDTkwgsv3KN6ampq9qg9HaRPY1KeNDU2ZvXq1aWupvMV/fyTwl+DQc3Nz/+hubmQ5w8AAACFVvDnIklcgwPh/IdtSLq0vKtqQM9UD+q12y4qK8pbdVxL1j71RPLcfnrt9mGPP/54u9sKyF6iqqoq69aty/z58zNlypTt9tXU1OSKK65IkowbNy5lZWU77eeGG27I6NGjM3HixD2qZ/DgwWaQ7QNqKirSlKS8oiKDhwwpdTmdrujnn7gGeeH7rqwsQ4p4/gAAAFBghX8uEtfgQDj/tV0b0/K0mOdnfe1K1YAeqawoT0NjU2rrdtbLrvsaeEj3dO27f167fU1TU9O2yUWDBg1qdz8CspeYNm1alixZkquvvjrTp0/PqFGjkiRz587NhRdemLq6uiTJhAkTdtrHX/7yl8ybNy9XXnnlHtezdOnS9OrVvkSajlM9bWZWP7Ehg6sGZ9WfV5W6nE5X9PNPXIPJtyRNSSrKK7a9hxEAAAAohqI/F0lcgwPh/L90w5/zoS/8ocV9LS2J+GIrbz8v1YN6pbZuY4ZO/2Gbx66oKMuKh+5Nj+4imY5QX1+f3r17J0nuueeedvdjatJLzJgxI/3798/KlSszduzYHHvssRk5cmQmT56c4cOH5+STT06y6/eP3XDDDSkrK8sFF1zQWWUDAAAAAAA7cdzo/iUbe8zwg4Vj+yAB2UtUV1fn7rvvzumnn57u3btn+fLl6devX66//vrMnj07Dz/8cJKdB2TNzc35/ve/n5NOOimHH354Z5YOAAAAAAC04LgxA9Kn105eQraXTZ00uCTjsmsiyxaMHj06t9566w7b169fn+XLl6e8vDzHHHNMi21/+9vfZsWKFfnEJz6xt8sEAAAAAABaoVfPLrnwjBH5yqwlnT72e849utPHZPfMIGuDRYsWpbm5OSNHjkzPnj1bPOaGG25Ijx498pa3vKWTqwMAAAAAAHbmfW/t/KDqpEmDM+aoQzp9XHZPQNYGDz74YJKdL6+4adOm3HjjjTn77LPTp0+fziwNAAAAAADYhWNG9ss504Z16pj/+p4JnToerWeJxTbYXUDWvXv3PP30051YEQAAAAAA0Fpf/tircte8mjz19Oa9Ptb73np0pk4+bK+PQ/uYQdYGuwvIAAAAAACAfdeg/j3y5Y++qk1taus2ZNXj9amt29DqNkcO6ZPPf2hyW8ujE5lB1gZz5swpdQkAAAAAAMAeeNupw/PIX5/Nv1x3f6uOn3T+LW3q/9B+3fOLr74+vXt2aU95dBIzyAAAAAAAgEL52N9PyFWXTezwfocc2jN3fev0jBp2UIf3TccSkAEAAAAAAIXzkYvH5+b/nJZB/Xt0SH9nvvbwzJ35xowefnCH9MfeJSADAAAAAAAK6Y1Tj8iin745F54xImVl7etj4CHd873Pnpj/vXZaBg/s2bEFstcIyAAAAAAAgMLqf3D3fO/K1+bRn781/3zx+Aw8pHur2r365YPy/atOysrbz8uFZ45MWXsTNkqistQFAAAAAAAAlNqwIX1y5WUT8+lLXpGHlj+TeYvqsuAvT2XtM5uztaEpPbpXZviQPjluzIAcN6Z/BvbrmKUZKQ0BGQAAAAAAwP9XUVGeMUcdkjFHHZJ3nDWy1OWwl1hiEQAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAFrU3Nyc5ubmbX+GA0VlqQsAAAAAAAD2DbV1G3Lj7csz989P5v7FdfnL8mfS2Ph8MLbmyY2ZdP7/ZuKYATl+3KF587Qj0qdX1xJXDO0jIAMAAAAAgIK7b+Hj+a8fLM5NdyzP1oamnR43b1Fd5i2qy9d+/Jf8w+e65B1njsg/nD8mLzvy4M4rFjqAJRYBAAAAAKCgnnluSy7+xN159TtuzQ9/8eguw7GXeq5+a778wyU59pyf5tPXL8jWra1vC6UmIAMAAAAAgAK6+/7aHPPmn+RbP314j/rZ2tCUf/3y/LzyglvyyF+f7aDqYO8SkAEAAAAAQMH8/O6Vmf6eX2TV4/Ud1ueCvzyVEy66NX9eurbD+oS9RUAGAAAAAAAFctfcmrz5H+/M5i2NHd73409tzPT3/CKPrjKTjH2bgAwAAAAAAAriqac35bwZv94r4dgLaus25m8+fFcaG72TjH2XgAwAAAAAAAri0s/9Po8/tbFNbebOPCsrbz8vc2ee1eo2f3jwyXzxu39ua3nQaQRku1BXV5cZM2ZkxIgR6d69e4YOHZrLLrss9fX1ufjii1NWVpbrrruu1GUCAAAAAMBu3f671fnBz5e1uV3VgJ6pHtQrVQN6tqndv35lflasea7N40FnqCx1AfuqhQsX5rTTTkttbW169eqVMWPGZM2aNbn22muzbNmyrF37/EsGJ0yYUNpCAQAAAACgFf7jfzp3RtfmLY25/scP5crLJnbquNAaZpC1oK6uLmeeeWZqa2tz+eWXp6amJvPnz09tbW2uvvrqzJ49O3Pnzk1ZWVnGjRtX6nIBAAAAAGCXHl31bG67Z1Wnj/vfP3lor77vDNpLQNaCSy+9NKtWrcoll1ySa665Jn369Nm2b8aMGRk/fnwaGhoybNiw9O3bt4SVAgAAAADA7n1/9rI0N3f+uE+u25Rf3tv5wRzsjoDsJZYsWZJZs2ZlwIABueqqq1o85rjjjkuSjB8/frvtd999d173utdlwIABOfjgg3P88cfnJz/5yV6vGQAAAAAAduWPDz5ZurH/XLqxYWcEZC8xc+bMNDU15YILLkjv3r1bPKZHjx5Jtg/IHnjggUyfPj0VFRX5zne+k1mzZmXo0KF5y1vekltvvbVTagcAAAAAgJbcv+Sp0o29uHRjw85UlrqAfc2cOXOSJFOnTt3pMatWPT8d9MUB2axZs1JWVpabb745PXv2TJJMmzYtw4cPz/e///2cccYZe7FqAAAAAABo2bpnN6fmyQ0lG//Pj6wr2diwMwKyl1ixYkWS5Igjjmhxf0NDQ+69994k2wdkW7ZsSdeuXbfNLkuSioqK9OnTJ01NTe2uZ+TIkSkvN9Gv1GoO/lBSflBqamtSXV1d6nI6XdHPP3ENBl23ImXlFWlsakx1dcvfjwAAAMCBqejPRZL9/xo0lvVNDrl8p/vnzjwrVQN67nR/1YAe235feft5Oz2utm5DJp1/yw7bV9c8uV9eN/ZNL85cTjjhhCxYsKBd/QjIXqK+vj5JsnHjxhb3z5o1K3V1denTp0+OPPLIbdsvvPDCfPnLX87ll1+eD3/4w6msrMz111+fpUuX5itf+Uq766mpqWl3WzpQn8akPGlqbMzq1atLXU3nK/r5J4W/BoNeeINrc3Mhzx8AAAAKreDPRZLs/9egckNyyM53Vw3omepBvXbfTUV5q457qeYmz5TYOx5//PF2txWQvURVVVXWrVuX+fPnZ8qUKdvtq6mpyRVXXJEkGTduXMrKyrbtGz9+fO688868+c1vzpe+9KUkSa9evfLjH/84J554YrvrGTx4sBlk+4Caioo0JSmvqMjgIUNKXU6nK/r5J65BXvi+KyvLkCKePwAAABRY4Z+LZP+/Bk1l3bOrqRi1dbtefrFqQI9UVpSnobEptXUtTy7ZVT8VZQ2p2g+vG/umpqambZOLBg0a1O5+BGQvMW3atCxZsiRXX311pk+fnlGjRiVJ5s6dmwsvvDB1dXVJkgkTJmzXbunSpXnb296WSZMm5f3vf38qKiry/e9/P+edd15uvfXWnHzyye2qZ+nSpenVq+2JPB2retrMrH5iQwZXDc6qP68qdTmdrujnn7gGk29JmpJUlFdsew8jAAAAUAxFfy6SHBjX4LDXzdzpe8haWhbxxVbefl6qB/VKbd3GDJ3+wzaPPf3Esbntq/vndWPfU19fn969eydJ7rnnnnb3IyB7iRkzZuQHP/hBVq5cmbFjx+boo4/Opk2b8sgjj+S0007LsGHD8stf/nK7948lyUc/+tH07NkzP/3pT1NZ+fxlPeWUU/LXv/41l19+ebvXwAQAAAAAgD113Oj+uXUnAdleH3tM/5KMC7ti7b6XqK6uzt13353TTz893bt3z/Lly9OvX79cf/31mT17dh5++OEk2SEge/DBBzN+/Pht4dgLJk6cmCVLlnRa/QAAAAAA8FKTjx1YurGPKd3YsDNmkLVg9OjRufXWW3fYvn79+ixfvjzl5eU55phjtttXVVWVhQsXpqGhYbuQbO7cud7XAwAAAABASb399BH5xFfmp7m5c8cdeEj3vP7V1Z07KLSCGWRtsGjRojQ3N2fkyJHp2bPndvs+8IEPZOnSpXnTm96UW2+9NbfddlsuvPDC/OY3v8lll11WoooBAAAAACA5srpP3vCaoZ0+7t+d87J061rR6ePC7gjI2uDBBx9MsuPyikly7rnn5mc/+1mefvrpXHTRRTn//PPz0EMP5fvf/34uvfTSzi4VAAAAAAC288G3j+3U8bp1rch7zj26U8eE1rLEYhvsKiBLkjPOOCNnnHFGZ5YEAAAAAACtMu34Ibng9KPy/dnLOmW8T3/gFTl8cO9OGQvaygyyNthdQAYAAAAAAPuyaz8yJYP692hTm9q6DVn1eH1q6za0us3x4wbmQ+84pq3lQacxg6wN5syZU+oSAAAAAACg3fod1C2zvjA1r3/vL7N5S2Or2kw6/5Y2jVE1oEe+/7mTUlFhjg77Lv86AQAAAACgQF47cXB+8qXXpXu3ig7vu2pAj9zx9dMyvLpvh/cNHUlABgAAAAAABfOG1wzNr752aqoH9eqwPl9+dP/c/Z0zMnbEIR3WJ+wtAjIAAAAAACig1xxXlT//5M25+E2j9qifLpXl+dQHXpE/fP+sjDjczDH2DwIyAAAAAAAoqIP6dM1/f/I1ufd7Z+T804anS2XrY4M+vbrkkvPH5MGb3pSPv+fl6dJF5MD+o7LUBQAAAAAAAKX1qgmD8qoJg/KlGRtz4+2PZd6iuty/uC5LHns6DQ3NSZLu3SoybmS/HDemf1557KE5Z/qw9O7ZpcSVQ/sIyAAAAAAAgCTJoP498oHzxmz73NzcnK0NTSkvK0tlG2aXwb5OQAYAAAAAALSorKwsXbtUlLoM6HDiXgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABSKgAwAAAAAAIBCEZABAAAAAABQKAIyAAAAAAAACkVABgAAAAAAQKEIyAAAAAAAACgUARkAAAAAAACFIiADAAAAAACgUARkAAAAAAAAFIqADAAAAAAAgEIRkAEAAAAAAFAoAjIAAAAAAAAKRUAGAAAAAABAoQjIAAAAAAAAKBQBGQAAAAAAAIUiIAMAAAAAAKBQBGQAAAAAAAAUioAMAAAAAACAQhGQAQAAAAAAUCgCMgAAAAAAAApFQAYAAAAAAEChCMgAAAAAAAAoFAEZAAAAAAAAhSIgAwAAAAAAoFAEZAAAAAAAABRKZakLAAAAAACAfUVzc3NWrFmf+xfXZf6Sp7L6iQ1Z+8zmJMkz67fkR798NMeNGZDh1X1SVlZW4mqB9hKQAQAAAABQeE8/uznfvWVpvvqjv+Sh5c+0eMz6DQ152xW/TpIMr+6T9557dP72TaPS/+DunVkq0AEssQgAAAAAQGFt3dqUz3x9QYZM+2E++Pk/7DQce6lHVz2XGV+amyHTfpiP/ue8bN7SuJcrBTqSgAwAAAAAgEJ68OG1Of7tt+Tj183Phk0N7epj85bGXPXNB/KKt92cuX9+soMrBPYWARkAAAAAAIXzi3tW5ZVvvyXzlzzVIf0tXvZ0Trjo1vzkjuUd0h+wdwnIAAAAAAAolNt/tzpvvOz2bNzUscsibtnalHP/aU5+eufyDu0X6HgCMgAAAAAACuORvz6bN33wjmzZ2rRX+m9qas55M36dBx7qmJlpwN4hIAMAAAAAoBCamprzro//NvUb2/a+sbkzz8rK28/L3Jlnter4LVub8s6P/zZb91IIB+w5ARkAAAAAAIXwXz9YlHsWPN7mdlUDeqZ6UK9UDejZ6jYL/7I2n/vWA20eC+gcArJdqKury4wZMzJixIh07949Q4cOzWWXXZb6+vpcfPHFKSsry3XXXVfqMgEAAAAA2I0tWxtz5X93bmB1zXcfzPoNWzt1TKB1KktdwL5q4cKFOe2001JbW5tevXplzJgxWbNmTa699tosW7Ysa9euTZJMmDChtIUCAAAAALBbP7ljeZ5Yu6lTx3x2/db84OfL8vdvObpTxwV2zwyyFtTV1eXMM89MbW1tLr/88tTU1GT+/Pmpra3N1VdfndmzZ2fu3LkpKyvLuHHjSl0uAAAAAAC78bUf/6Uk4371R0tKMi6wawKyFlx66aVZtWpVLrnkklxzzTXp06fPtn0zZszI+PHj09DQkGHDhqVv374lrBQAAAAAgN3ZvKUx9y18oiRjL/zL2jz97OaSjA3snIDsJZYsWZJZs2ZlwIABueqqq1o85rjjjkuSjB8/frvtd9xxR44//vh07949hx56aN773vfmmWee2es1AwAAAACwcw8uXZutDU0lG3/+kqdKNjbQMgHZS8ycOTNNTU254IIL0rt37xaP6dGjR5LtA7Lf/OY3OfXUUzNkyJD89Kc/zWc/+9nceOONOfvss9Pc3NwptQMAAAAAsKNSB1SlHh/YUWWpC9jXzJkzJ0kyderUnR6zatWqJNsHZJ/61KcycuTI/PjHP055+fO5Y//+/XPOOedk9uzZOeOMM9pVz8iRI7f1R+nUHPyhpPyg1NTWpLq6utTldLqin3/iGgy6bkXKyivS2NSY6uojSl0OAAAA0IkOhOciz3U/Men5uhb3zZ15VqoG9Nxl+6oBPbb9vvL283Z6XG3dhkw6/5Ydtn/ys/+e//iX29tQMbAzTU3/Nxv0hBNOyIIFC9rVj4DsJVasWJEkOeKIlh8ANzQ05N57702yfUD2hz/8Ie9617u2C7NOOeWUJMnNN9/c7oCspqamXe3oYH0ak/KkqbExq1evLnU1na/o558U/hoMemEmbHNzIc8fAAAACu1AeC5yaH2ykwysakDPVA/q1apuKivKW33si62v35j1NfvptYN92OOPP97utgKyl6ivr0+SbNy4scX9s2bNSl1dXfr06ZMjjzxy2/aKiop07dp1u2O7dOmSsrKyLFq0qN31DB482AyyfUBNRUWakpRXVGTwkCGlLqfTFf38E9cgZWXbfh9SxPMHAACAAjsQnos8171nnt3Jvtq6DbttXzWgRyorytPQ2JTaupafHe+qr969uueg/fTawb6mqalp2+SiQYMGtbsfAdlLVFVVZd26dZk/f36mTJmy3b6amppcccUVSZJx48al7IUHxklGjRqVP/zhD9sdP3fu3DQ3N2ft2rXtrmfp0qXp1avtP5FAx6qeNjOrn9iQwVWDs+rPq0pdTqcr+vknrsHkW5KmJBXlFduWmQUAAACK4UB4LvL92Y/k7f/8mxb3tbQk4kutvP28VA/qldq6jRk6/YdtHv/zn/lw3ve277S5HbCj+vr69O7dO0lyzz33tLsfU5NeYtq0aUmSq6++Og8//PC27XPnzs3UqVNTV1eXJJkwYcJ27S699NLce++9+cxnPpO6urosXLgw73//+1NRUWEGGAAAAABACR03ZkChxwd2JLl5iRkzZqR///5ZuXJlxo4dm2OPPTYjR47M5MmTM3z48Jx88slJtn//WJK8/e1vz4c//OF8+tOfzsCBAzNx4sRMnTo1EyZMyODBg0txKgAAAAAAJBl1xEHp3bNLScaurCzLuFGHlGRsYOcEZC9RXV2du+++O6effnq6d++e5cuXp1+/frn++usze/bsbbPKXhqQlZWV5XOf+1zq6urywAMP5PHHH88Xv/jFLF26NK961atKcSoAAAAAACQpLy/L6SdWl2Ts100+LN27edsR7Gv8V9mC0aNH59Zbb91h+/r167N8+fKUl5fnmGOOabFtnz59Mm7cuCTJN77xjWzcuDHvete79mq9AAAAAADs2vveOjqzfvFYp4/7gfPGdPqYwO4JyNpg0aJFaW5uzqhRo9KzZ8/t9s2bNy+33357XvGKV6ShoSF33HFHrr322lxzzTU56qijSlQxAAAAAABJcuJxVRl71MFZtOzpThvz8MG98obXlGbmGrBrArI2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vvnm/OhHP9q2jGJZWVkmTJiQU045Jccee2wqK1v+X4Vnn30299xzT371q1+ltrY2SXL33XfnT3/6U3r37p3Vq1cn+b9wbOjQoZ1zUu1UlHshAAAAAM8r1Ayyurq6nHnmmamtrc3ll1+empqazJ8/P7W1tbn66qsze/bszJ07N2VlZRk3blypyy2p733ve7nhhhsyZMiQvPOd78zxxx+fX/ziF/nCF76Qpqambcf169cvw4cPz7x580pYbevMvntlVtbWd/q4t/9uTR5e/kynj9teK9Y8l+Pf/rMdwrEXe65+az5+3fy899P37vBuIvZ/v38ief/vWg7HXlC7MblibnLr/jtBsvCam5vzP//zP5k1a9a2/46POuqofP7zn8+HP/zhvPzlL99pOJY8H3y94Q1vyL//+7/nve99b3r27JkkeeaZZ/a7cGxnDsR7IQAAAADPK9QMsksvvTSrVq3KJZdckmuuuWa7fTNmzMgPfvCDPPDAAznyyCPTt2/fElVZeitXrswvf/nLTJ48OR/60Ie2bT/00EPzne98J/fdd19OOOGEbdsnTpyYn/zkJ9m0adN2P1G/r/nlvatKNvbtv1+dUcMOKtn4rdXU1JyzLr0jqx5vXZD49RsfyrEj++WS88fs5croLLUbkhlzky1Nuz+2OcmnH0iG903GHLy3K6Oj3XzzzZk9e/a2z+eee27OPvvsVFRUtKmf8vLynHTSSRkxYkT+5V/+JZs2bdq2793vfvd+G44dqPdCAAAAAJ5XmBlkS5YsyaxZszJgwIBcddVVLR5z3HHHJUnGjx+/bdsLgdrkyZPTrVu3nS4nd+ONN+acc87JEUcckZ49e+boo4/Oxz72saxfv4spGPuo++67L83NzTnttNO2237yySenW7duueeee7bbPnHixGzdujULFy7sxCrb7v7FT5Vs7HmL6nZ/0D7gl/euyp8eXtumNtd898E0NrYiTWG/cOPyZENj649vbE5mLttr5ZTchsbG1G3e3OKv/dnSpUvzox/9aNvnd7/73TnnnHPaHI69oL6+Pl/96le3C8eS5++NDQ0Ne1RrqRyo90IAAAAAnleYgGzmzJlpamrKBRdckN69e7d4TI8ePZJsH5A98sgjuemmm1JVVZVJkybttP9rrrkmFRUVufLKK3Pbbbflfe97X7761a/m1FNP3W4Zpv3BsmXLUlZW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nv+EmhuzevW+cW49KyqSCR3X34jevfO6gYM6rsMWrFmzJhsaW//et93Z1ffgnXfeue19eKeddloOPvjgdo2xq3AsSaqrq3PCCSfkrrvuysaNG3Pvvffmda97XYt97evfg03//++mqfH5f+cv/byv6Yj74Av9uBcCAAAA7Nsef/zxdrctTEBWVVWVdevWZf78+ZkyZfvZFTU1NbniiiuSJOPGjWv3bIIkee1rX7ttdsKIESMyZ86cDBw4sN39DR48uM0zyPbUIYccklWrVmXr1q07LC21du3a9OnTZ4efqp83b1569uyZMWPGtGmsww47zE/Nd5KnupVl00721dZt2GXbqgE9UllRnobGptTWtRwy766v/of0Svc+Q1pTask8131LWn4cvmuVTU9n0JB949x67MGs11I57LDDOnwGWUuam5vz61//OklSUVGxyyV3d2V34dgLTjnllNx1111Jnl/md2cB2b7+PVj+/5cKLK+oyJAhQ3b4vK/piPtg4l4IAAAAsK9qamraNrlo0KD2/3B/YQKyadOmZcmSJbn66qszffr0jBo1Kkkyd+7cXHjhhamrq0uSTJgwYY/G+eY3v5mnn346jz32WL7whS/klFNOyb333pvDDz+8Xf0tXbo0vXq1fsZOQ0NDbrrppnaN9YKjjjoqf/rTn/LII49k9OjR27Zv2bIlK1asyNFHH71Dm3nz5mXChAltXk7y4Ycf3qMlKGm9T1+/IP/65fkt7mtpScQXW3n7eake1Cu1dRszdPoP2zX+4vt/lUP792hX286y5on6HP76WWlsbG5Tuy/881n54IUf3UtVtU3zpk1peOtFpS6jTR5++OGU7WT5wfbY2ffgE088kaeffjpJcswxx6Rfv35t7ru14ViSDB8+PEOGDMnq1auzfPnybNmypcXwZl//Hrzyy9/Ps+vrM7hqcFatWrXD531NR9wHE/dCAAAAgH1VfX19evfunSS555572t3P/jfVoJ1mzJiR/v37Z+XKlRk7dmyOPfbYjBw5MpMnT87w4cNz8sknJ9n+/WPt8bKXvSyvfOUrc9555+XOO+/Mc889l89//vMdcQqdZsqUKSkrK8ttt9223fY5c+Zk8+bNOeGEE7bbvnr16tTU1GTixImdWSZtdNyYASUbu3pQr30+HEuSww7tlTed3PJ7CnemR/eKXPTGkXupIjrSY489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AAAAAAAAAEMhIAMAAAAAAAAAAIChEJABAAAAAAAAAADAUAjIAAAAAAAAAAAAYCgEZAAAAAAAAAAAADAUAjIAAAAAAAAAAAAYCgEZAAAAAAAAAAAADIWADAAAAAAAAAAAAIZCQAYAAAAAAAAAAABDISADAAAAAAAAAACAoRCQAQAAAAAAAAAAwFAIyAAAAAAAAAAAAGAoBGQAAAAAAAAAAAAwFAIyAAAAAAAAAAAAGAoBGQAAAAAAAAAAAAyFgAwAAAAAAAAAAACGQkAGAAAAAAAAAAAAQyEgAwAAAAAAAAAAgKEQkAEAAAAAAAAAAMBQCMgAAAAAAAAAAABgKARkAAAAAAAAAAAAMBQCMgAAAAAAAAAAABgKARkAAAAAAAAAAAAMhYAMAAAAAAAAAAAAhkJABgAAAAAAAAAAAEMhIAMAAAAAAAAAAIChEJABAAAAAAAAAADAUAjIAAAAAAAAAAAAYCgEZAAAAAAAAAAAADAUAjIAAAAAAAAAAAAYCgEZAAAAAAAAAAAADIWADAAAAAAAAAAAAIZCQAYAAAAAAAAAAABDISADAAAAAAAAAACAoRCQAQAAAAAAAAAAwFAIyAAAAAAAAAAAAGAoBGQAAAAAAAAAAAAwFAIyAAAAAAAAAAAAGAoBGQAAAAAAAAAAAAyFgAwAAAAAAAAAAACGQkAGAAAAAAAAAAAAQyEgAwAAAAAAAAAAgKEQkAEAAAAAAAAAAMBQCMgAAAAAAAAAAABgKARkAAAAAAAAAAAAMBQCMgAAAAAAAAAAABgKARkAAAAAAAAAAAAMhYAMAAAAAAAAAAAAhkJABgAAAAAAAAAAAEMhIAMAAAAAAAAAAIChEJABAAAAAAAAAADAUAjIAAAAAAAAAAAAYCgEZAAAAAAAAAAAADAUAjIAAAAAAAAAAAAYCgEZAAAAAAAAAAAADIWADAAAAAAAAAAAAIZCQAYAAAAAAAAAAABDISADAAAAAAAAAACAoRCQAQAAAAAAAAAAwFAIyAAAAAAAAAAAAGAoBGQAAAAAAAAAAAAwFAIyAAAAAAAAAAAAGAoBGQAAAAAAAAAAAAyFgAwAAAAAAAAAAACGQkAGAAAAAAAAAAAAQyEgAwAAAAAAAAAAgKEQkAEAAAAAAAAAAMBQCMgAAAAAAAAAAABgKARkAAAAAAAAAAAAMBQCMgAAAAAAAAAAABgKARkA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", + "text/plain": [ + "
" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "## Estimating the energy on a quantum computer\n", - "\n", - "For the final stage of this state preparation application benchmark workflow, we estimate the energy of the optimized multi-configuration wavefunction prepared on a quantum computer.\n", - "The first step in this process is mapping the classical Hamiltonian for the active space to a qubit Hamiltonian that can be measured on a quantum computer.\n", - "For this example, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation) to perform this mapping." + "qc = select_circuit.get_qiskit_circuit()\n", + "num_shots = 10000\n", + "num_qubits = select_circuit._qsharp_factory.parameter[\"numQubits\"]\n", + "qc.draw(\"mpl\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "2f856f41", + "execution_count": 16, + "id": "0f664edf", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Prepare qubit Hamiltonian\n", - "qubit_mapper = create(\"qubit_mapper\", algorithm_name=\"qiskit\", encoding=\"jordan-wigner\")\n", - "qubit_hamiltonian = qubit_mapper.run(hamiltonian)" + "import numpy as np\n", + "from qiskit.circuit import ClassicalRegister\n", + "from qiskit import transpile\n", + "from qiskit_aer import AerSimulator\n", + "\n", + "# Copy circuit & add measurement on system qubits only\n", + "full_qc = qc.copy()\n", + "data_creg = ClassicalRegister(num_qubits, \"data\")\n", + "full_qc.add_register(data_creg)\n", + "full_qc.measure(list(range(num_qubits)), data_creg)" ] }, { - "cell_type": "markdown", - "id": "ea443c34", + "cell_type": "code", + "execution_count": 17, + "id": "e1692950", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Qiskit AerSimulator: 12 system qubits, 10000 shots\n", + "\n", + "Bitstring Expected Measured\n", + "------------------------------------\n", + " 000111000111 0.6203 0.6275\n", + " 001011001011 0.3365 0.3296\n", + " 100101100101 0.0108 0.0107\n", + " 010110010110 0.0099 0.0090\n", + " 101001101001 0.0057 0.0063\n", + " 001101100011 0.0056 0.0056\n", + " 100011001101 0.0056 0.0066\n", + " 011010011010 0.0055 0.0047\n", + "\n", + "Fidelity: 0.9998\n" + ] + } + ], "source": [ - "### Optimizing the Hamiltonian\n", + "from qiskit_aer import AerSimulator\n", + "# Simulate\n", + "sim = AerSimulator()\n", + "full_qc = transpile(full_qc, sim, optimization_level=0)\n", + "result = sim.run(full_qc, shots=num_shots).result()\n", + "counts = result.get_counts()\n", "\n", - "Recall from earlier in the notebook that the full Hamiltonian included 1000+ one- and two-body integrals.\n", - "Despite optimizing the wavefunction, our Hamiltonian still contains a large number of terms that would need to be measured on a quantum computer to estimate the energy.\n", - "The cell below illustrates this point." + "# Build expected probability distribution from wfn_sparse\n", + "coeffs = wfn_sparse.get_coefficients()\n", + "dets = wfn_sparse.get_active_determinants()\n", + "num_orbitals = len(wfn_sparse.get_orbitals().get_active_space_indices()[0])\n", + "\n", + "expected_probs = {}\n", + "norm_sq = sum(abs(c) ** 2 for c in coeffs)\n", + "for coeff, det in zip(coeffs, dets):\n", + " alpha_str, beta_str = det.to_binary_strings(num_orbitals)\n", + " # Our bitstring: position i = qubit i (little-endian, LSB at index 0)\n", + " bitstring = beta_str[::-1] + alpha_str[::-1]\n", + " qiskit_bs = bitstring\n", + " expected_probs[qiskit_bs] = expected_probs.get(qiskit_bs, 0.0) + abs(coeff) ** 2 / norm_sq\n", + "\n", + "# Build simulated probability distribution from shot counts\n", + "sim_probs = {}\n", + "for key, count in counts.items():\n", + " # If multiple classical registers, Qiskit joins them with spaces,\n", + " # ordered last-added-first (leftmost). data_creg was added last,\n", + " # so it is the first token.\n", + " data_bits = key.split()[0] if \" \" in key else key\n", + " sim_probs[data_bits] = sim_probs.get(data_bits, 0.0) + count / num_shots\n", + "\n", + "# Classical fidelity (Bhattacharyya coefficient)\n", + "all_keys = set(expected_probs.keys()) | set(sim_probs.keys())\n", + "bc_sum = sum(np.sqrt(expected_probs.get(k, 0.0) * sim_probs.get(k, 0.0)) for k in all_keys)\n", + "classical_fidelity = float(bc_sum ** 2)\n", + "\n", + "# Print comparison table\n", + "print(f\"Qiskit AerSimulator: {num_qubits} system qubits, {num_shots} shots\\n\")\n", + "print(f\"{'Bitstring':<{num_qubits + 2}} {'Expected':>10} {'Measured':>10}\")\n", + "print(\"-\" * (num_qubits + 24))\n", + "\n", + "all_bs = sorted(all_keys, key=lambda x: expected_probs.get(x, 0), reverse=True)\n", + "for bs in all_bs:\n", + " exp_p = expected_probs.get(bs, 0.0)\n", + " meas_p = sim_probs.get(bs, 0.0)\n", + " if exp_p > 0.001 or meas_p > 0.001:\n", + " marker = \" *\" if exp_p < 0.001 else \"\"\n", + " print(f\" {bs} {exp_p:>10.4f} {meas_p:>10.4f}{marker}\")\n", + "\n", + "print(f\"\\nFidelity: {classical_fidelity:.4f}\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "c912f36e", + "id": "4a1586f7", "metadata": {}, "outputs": [], "source": [ - "# Print the number of Pauli strings in the full Hamiltonian\n", - "print(f\"Number of Pauli strings in the Hamiltonian: {len(qubit_hamiltonian.pauli_strings)}\")" + "qsharp.eval(\"\"\"import Std.Measurement.MeasureEachZ;\n", + "\n", + "operation VerifyBinaryEncodingStatePrep(\n", + " rowMap : Int[],\n", + " stateVector : Double[],\n", + " expansionOps : MatrixCompressionOp[],\n", + " binaryEncodingOps : MatrixCompressionOp[],\n", + " numQubits : Int,\n", + " numAncilla : Int,\n", + ") : Result[] {\n", + " use qs = Qubit[numQubits + numAncilla];\n", + " BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams {\n", + " rowMap = rowMap,\n", + " stateVector = stateVector,\n", + " expansionOps = expansionOps,\n", + " binaryEncodingOps = binaryEncodingOps,\n", + " numQubits = numQubits,\n", + " numAncilla = numAncilla,\n", + " }, qs);\n", + " let results = MeasureEachZ(qs[0..numQubits - 1]);\n", + " ResetAll(qs);\n", + " return results\n", + "}\"\"\")" ] }, { - "cell_type": "markdown", - "id": "f5a7c250", + "cell_type": "code", + "execution_count": null, + "id": "739b5481", "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0b6ce6cf72e1492e80b07117237eccd5", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "However, we can optimize this measurement process in two ways:\n", - "\n", - "1. Applying the classical wavefunction information to pre-screen the qubit Hamiltonian, identifying which terms actually need quantum measurements. The remaining terms are converted into precomputed classical coefficients, allowing us to slash the number of quantum measurements required for the sparse state.\n", - "2. Grouping commuting terms in the Hamiltonian to reduce the number of measurements.\n", + "import qdk\n", + "from qdk.widgets import Histogram\n", "\n", - "The following cell shows how to perform these optimizations using the `qdk-chemistry` library, resulting in a much more efficient measurement process for estimating the energy on a quantum computer." + "# --- Run the circuit in the simulator and collect measurement statistics ---\n", + "shots = 1000\n", + "params = circuit._qsharp_factory.parameter\n", + "Histogram(qsharp.run(\n", + " qdk.code.VerifyBinaryEncodingStatePrep, shots, *params.values()\n", + "))" ] }, { "cell_type": "code", "execution_count": null, - "id": "624cca8b", + "id": "146f56e5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "System qubits only (ancilla traced out). 12 qubits, 1000 shots.\n", + "\n", + "Bitstring Expected Measured\n", + "------------------------------------\n", + " 111000111000 0.6203 0.6120\n", + " 110100110100 0.3365 0.3340\n", + " 101001101001 0.0108 0.0110\n", + " 011010011010 0.0099 0.0130\n", + " 100101100101 0.0057 0.0070\n", + " 101100110001 0.0056 0.0100\n", + " 110001101100 0.0056 0.0040\n", + " 010110010110 0.0055 0.0090\n", + "\n", + "Fidelity (Bhattacharyya): 0.9985 (1.0 = perfect match)\n" + ] + } + ], "source": [ - "from qdk_chemistry.data.qubit_hamiltonian import filter_and_group_pauli_ops_from_wavefunction\n", + "import numpy as np\n", + "from collections import Counter\n", + "import qdk\n", + "\n", + "# --- Run the circuit in the simulator and collect measurement statistics ---\n", + "shots = 1000\n", + "params = circuit._qsharp_factory.parameter\n", + "raw_results = qsharp.run(\n", + " qdk.code.VerifyBinaryEncodingStatePrep, shots, *params.values()\n", + ")\n", + "\n", + "# Convert each shot's Result[] to a bitstring.\n", + "# MeasureEachZ(qs[0..N-1]) returns [result_q0, result_q1, ...].\n", + "# Our bitstring convention: position i = qubit i. No reversal needed.\n", + "measured_bitstrings = [\n", + " \"\".join(\"1\" if str(r) == \"One\" else \"0\" for r in one_run)\n", + " for one_run in raw_results\n", + "]\n", + "counts = Counter(measured_bitstrings)\n", + "\n", + "# --- Build expected probability distribution from wfn_sparse (system qubits only) ---\n", + "coeffs = wfn_sparse.get_coefficients()\n", + "dets = wfn_sparse.get_active_determinants()\n", + "num_orbitals = len(wfn_sparse.get_orbitals().get_active_space_indices()[0])\n", + "\n", + "expected_probs = {}\n", + "norm_sq = sum(abs(c) ** 2 for c in coeffs)\n", + "for coeff, det in zip(coeffs, dets):\n", + " alpha_str, beta_str = det.to_binary_strings(num_orbitals)\n", + " # Position i = qubit i (same convention as sparse_isometry _run_impl)\n", + " bitstring = beta_str + alpha_str\n", + " expected_probs[bitstring] = abs(coeff) ** 2 / norm_sq\n", "\n", - "# Filter and group Pauli operators based on the wavefunction\n", - "filtered_hamiltonian_ops, classical_coeffs = (\n", - " filter_and_group_pauli_ops_from_wavefunction(\n", - " qubit_hamiltonian,\n", - " wfn_sparse\n", + "# --- Compare ---\n", + "n_qubits = params[\"numQubits\"]\n", + "print(f\"System qubits only (ancilla traced out). {n_qubits} qubits, {shots} shots.\\n\")\n", + "print(f\"{'Bitstring':<{n_qubits + 2}} {'Expected':>10} {'Measured':>10}\")\n", + "print(\"-\" * (n_qubits + 24))\n", + "\n", + "all_bs = sorted(\n", + " set(list(expected_probs.keys()) + list(counts.keys())),\n", + " key=lambda x: expected_probs.get(x, 0),\n", + " reverse=True,\n", + ")\n", + "for bs in all_bs:\n", + " exp_p = expected_probs.get(bs, 0.0)\n", + " meas_p = counts.get(bs, 0) / shots\n", + " if exp_p > 0.001 or meas_p > 0.001:\n", + " marker = \" *\" if exp_p < 0.001 else \"\"\n", + " print(f\" {bs} {exp_p:>10.4f} {meas_p:>10.4f}{marker}\")\n", + "\n", + "# --- Classical fidelity (Bhattacharyya coefficient squared) ---\n", + "fidelity = (\n", + " sum(\n", + " np.sqrt(expected_probs.get(bs, 0) * counts.get(bs, 0) / shots)\n", + " for bs in set(list(expected_probs.keys()) + list(counts.keys()))\n", " )\n", + " ** 2\n", ")\n", - "print(f\"Filtered and grouped qubit Hamiltonian contains {len(filtered_hamiltonian_ops)} groups:\")\n", - "for igroup, group in enumerate(filtered_hamiltonian_ops):\n", - " print(f\"Group {igroup+1}: {[group.pauli_strings]}\")\n", - "print(f\"Number of classical coefficients: {len(classical_coeffs)}\")" + "print(f\"\\nFidelity (Bhattacharyya): {fidelity:.4f} (1.0 = perfect match)\")" ] }, { - "cell_type": "markdown", - "id": "0dc7b827", + "cell_type": "code", + "execution_count": null, + "id": "62d03ae3", "metadata": {}, + "outputs": [], "source": [ - "### Estimating the energy on a quantum computer (simulator)\n", - "\n", - "Finally, we need to generate the measurement circuits required to estimate the energy of the prepared multi-configuration wavefunction on a quantum computer.\n", - "Since the optimized benzene diradical Hamiltonian contains only two measurement groups, we only need two measurement circuits to estimate the energy.\n", - "The cell below demonstrates how to generate these measurement circuits using the `qdk-chemistry` library and how to use the QDK simulator to execute them.\n", - "\n", - "This cell provides a set budget of measurements (\"shots\") to be evenly divided between the measurement circuits.\n", - "The measurement process is probabilistic, so we obtain a distribution of results from each circuit.\n", - "These distributions are then combined to produce a final energy estimate, along with an uncertainty (variance) as reported below.\n", - "The uncertainty is directly related to the number of shots used in the measurement process: more shots lead to lower uncertainty." + "# Bug found: GF2+X operations use \"cnot\" tag, but expansion ops filter checked for \"cx\".\n", + "# Fixed in sparse_isometry_binary_encoding.py to accept both.\n", + "# Restart kernel and re-run from the top to pick up the fix." ] }, { "cell_type": "code", "execution_count": null, - "id": "3e9b6616", + "id": "9594e049", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "numQubits: 12\n", + "numAncilla: 0\n", + "rowMap: [0, 2, 1]\n", + "stateVector length: 8\n", + "expansionOps count: 28\n", + "binaryEncodingOps count: 20\n", + "\n", + "--- Expansion Ops ---\n", + " [0] CX qubits=[4, 11]\n", + " [1] CX qubits=[4, 9]\n", + " [2] CX qubits=[4, 8]\n", + " [3] CX qubits=[4, 7]\n", + " [4] CX qubits=[4, 3]\n", + " [5] CX qubits=[4, 1]\n", + " [6] CX qubits=[3, 11]\n", + " [7] CX qubits=[3, 9]\n", + " [8] CX qubits=[3, 5]\n", + " [9] CX qubits=[3, 4]\n", + " ... (28 total)\n", + "\n", + "--- Binary Encoding Ops ---\n", + " [0] SELECT qubits=[0, 2, 1, 3] lookupData=8 rows\n", + " [1] CX qubits=[3, 1] lookupData=0 rows\n", + " [2] SELECT qubits=[0, 2, 1, 3] lookupData=8 rows\n", + " [3] CX qubits=[3, 2] lookupData=0 rows\n", + " [4] SELECT qubits=[0, 2, 1, 3] lookupData=8 rows\n", + " [5] CX qubits=[3, 1] lookupData=0 rows\n", + " [6] CX qubits=[3, 2] lookupData=0 rows\n", + " [7] CX qubits=[3, 0] lookupData=0 rows\n", + " [8] SWAP qubits=[2, 1] lookupData=0 rows\n", + " [9] SWAP qubits=[0, 4] lookupData=0 rows\n", + " ... (20 total)\n", + "\n", + "--- GF2+X result operations (from gf2x_result) ---\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2026-04-02 15:47:10.039960] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", + "[2026-04-02 15:47:10.062632] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", + "[2026-04-02 15:47:10.074259] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", + "[2026-04-02 15:47:10.083060] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", + "[2026-04-02 15:47:10.099927] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n", + "Total GF2+X operations: 28\n", + "Operation types: {'cx': 28}\n", + "row_map: [0, 2, 1, 3, 4]\n" + ] + } + ], "source": [ - "# Estimate energy using the optimized circuit and filtered Hamiltonian operators\n", - "estimator = create(\"energy_estimator\", algorithm_name=\"qdk\")\n", - "circuit_executor = create(\"circuit_executor\", algorithm_name=\"qdk_sparse_state_simulator\")\n", - "energy_results, simulation_data = estimator.run(\n", - " circuit=sparse_isometry_circuit,\n", - " qubit_hamiltonians=filtered_hamiltonian_ops,\n", - " circuit_executor=circuit_executor,\n", - " total_shots=250000,\n", - ")\n", + "# Diagnostic: inspect what the circuit parameters contain\n", + "params = circuit._qsharp_factory.parameter\n", + "print(f\"numQubits: {params['numQubits']}\")\n", + "print(f\"numAncilla: {params['numAncilla']}\")\n", + "print(f\"rowMap: {params['rowMap']}\")\n", + "print(f\"stateVector length: {len(params['stateVector'])}\")\n", + "print(f\"expansionOps count: {len(params['expansionOps'])}\")\n", + "print(f\"binaryEncodingOps count: {len(params['binaryEncodingOps'])}\")\n", "\n", + "# Show expansion ops\n", + "print(\"\\n--- Expansion Ops ---\")\n", + "for i, op in enumerate(params['expansionOps'][:10]):\n", + " print(f\" [{i}] {op['name']} qubits={op['qubits']}\")\n", + "if len(params['expansionOps']) > 10:\n", + " print(f\" ... ({len(params['expansionOps'])} total)\")\n", "\n", - "for i, results in enumerate(simulation_data.bitstring_counts):\n", - " print(f\"Measurement Results for Hamiltonian Group {i+1}: {simulation_data.hamiltonians[i].pauli_strings}\")\n", - " display(Histogram(bar_values=results))\n", + "# Show binary encoding ops\n", + "print(\"\\n--- Binary Encoding Ops ---\")\n", + "for i, op in enumerate(params['binaryEncodingOps'][:10]):\n", + " print(f\" [{i}] {op['name']} qubits={op['qubits']} lookupData={len(op.get('lookupData', [])) if op.get('lookupData') else 0} rows\")\n", + "if len(params['binaryEncodingOps']) > 10:\n", + " print(f\" ... ({len(params['binaryEncodingOps'])} total)\")\n", "\n", - "# Print statistic for measured energy\n", - "energy_mean = energy_results.energy_expectation_value + sum(classical_coeffs) + hamiltonian.get_core_energy()\n", - "energy_stddev = np.sqrt(energy_results.energy_variance)\n", - "print(\n", - " f\"Estimated energy from quantum circuit: {energy_mean:.3f} ± {energy_stddev:.3f} Hartree\"\n", + "# Check GF2+X operations directly\n", + "print(\"\\n--- GF2+X result operations (from gf2x_result) ---\")\n", + "gf2x_result, _ = sp._perform_gf2x(\n", + " [beta_str[::-1] + alpha_str[::-1]\n", + " for det in wfn_sparse.get_active_determinants()\n", + " for alpha_str, beta_str in [det.to_binary_strings(num_orbitals)]],\n", + " wfn_sparse.get_coefficients()\n", ")\n", - "\n", - "# Print comparison with reference energy\n", - "print(f\"Difference from reference energy: {energy_mean - E_sparse} Hartree\")" + "print(f\"Total GF2+X operations: {len(gf2x_result.operations)}\")\n", + "op_types = {}\n", + "for op in gf2x_result.operations:\n", + " op_types[op[0]] = op_types.get(op[0], 0) + 1\n", + "print(f\"Operation types: {op_types}\")\n", + "print(f\"row_map: {gf2x_result.row_map}\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d55a388b", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "qdk_chemistry_venv (3.12.3)", + "display_name": "qdk_chemistry_venv (3.13.12)", "language": "python", "name": "python3" }, @@ -519,7 +1318,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.13.12" } }, "nbformat": 4, diff --git a/python/src/qdk_chemistry/algorithms/registry.py b/python/src/qdk_chemistry/algorithms/registry.py index 84e072711..61411bcb5 100644 --- a/python/src/qdk_chemistry/algorithms/registry.py +++ b/python/src/qdk_chemistry/algorithms/registry.py @@ -585,6 +585,9 @@ def _register_python_algorithms(): from qdk_chemistry.algorithms.qubit_hamiltonian_solver import DenseMatrixSolver, SparseMatrixSolver # noqa: PLC0415 from qdk_chemistry.algorithms.qubit_mapper import QdkQubitMapper # noqa: PLC0415 from qdk_chemistry.algorithms.state_preparation import SparseIsometryGF2XStatePreparation # noqa: PLC0415 + from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import ( # noqa: PLC0415 + SparseIsometryBinaryEncodingStatePreparation, + ) from qdk_chemistry.algorithms.time_evolution.builder.partially_randomized import ( # noqa: PLC0415 PartiallyRandomized, ) @@ -600,6 +603,7 @@ def _register_python_algorithms(): register(lambda: QdkEnergyEstimator()) register(lambda: SparseIsometryGF2XStatePreparation()) + register(lambda: SparseIsometryBinaryEncodingStatePreparation()) register(lambda: DenseMatrixSolver()) register(lambda: SparseMatrixSolver()) register(lambda: QdkQubitMapper()) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py b/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py index 3c7453980..05a24d03c 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py @@ -11,6 +11,9 @@ from qdk_chemistry.algorithms.state_preparation.sparse_isometry import ( SparseIsometryGF2XStatePreparation, ) +from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import ( + SparseIsometryBinaryEncodingStatePreparation, +) from qdk_chemistry.algorithms.state_preparation.state_preparation import ( StatePreparation, StatePreparationFactory, @@ -18,6 +21,7 @@ ) __all__ = [ + "SparseIsometryBinaryEncodingStatePreparation", "SparseIsometryGF2XStatePreparation", "StatePreparationFactory", "StatePreparationSettings", diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index db79d5f71..84dd7e2fa 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -24,7 +24,7 @@ * SparseIsometryGF2X: Applies enhanced GF2+X elimination (preprocessing + GF2 + postprocessing), performs dense state preparation on the reduced space, - then applies recorded operations (CNOT and X) in reverse to expand back to + then applies recorded operations (CX and X) in reverse to expand back to the full space. """ @@ -33,7 +33,8 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Any import numpy as np @@ -65,14 +66,14 @@ class SparseIsometryGF2XStatePreparation(StatePreparation): the ``gf2x_with_tracking`` function which performs smart preprocessing before GF2 Gaussian elimination. The preprocessing includes: - 1. Removing duplicate rows using CNOT operations + 1. Removing duplicate rows using CX operations 2. Removing all-ones rows using X operations 3. Then performing standard GF2 Gaussian elimination 4. Apply the additional rank reduction if the reduced row-echelon matrix is diagonal This enhanced approach can be more efficient than standard GF2 Gaussian elimination, particularly for matrices with duplicate rows or all-ones rows. The algorithm - tracks both CNOT and X operations for proper circuit reconstruction. + tracks both CX and X operations for proper circuit reconstruction. The algorithm: @@ -80,7 +81,7 @@ class SparseIsometryGF2XStatePreparation(StatePreparation): 2. Converts bitstrings to a binary matrix 3. Applies enhanced GF2+X elimination (duplicate removal + all-ones removal + GF2) 4. Performs dense state preparation on the reduced space - 5. Applies recorded operations (both CNOT and X) in reverse order to expand back to full space + 5. Applies recorded operations (both CX and X) in reverse order to expand back to full space Key References: @@ -149,23 +150,20 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: return self._qiskit_dense_preparation(gf2x_operation_results, statevector_data, n_qubits) # Use QDK dense state preparation - expansion_ops: list[list[int]] = [] + expansion_ops: list[MatrixCompressionOp] = [] for operation in reversed(gf2x_operation_results.operations): - if operation[0] == "cnot": - # operation[1] should be a tuple for CNOT operations + if operation[0] == "cx": if isinstance(operation[1], tuple): target, control = operation[1] - expansion_ops.append([control, target]) + expansion_ops.append(MatrixCompressionOp("CX", [control, target])) elif operation[0] == "x" and isinstance(operation[1], int): - # operation[1] should be an int for X operations - qubit = operation[1] - expansion_ops.append([qubit]) + expansion_ops.append(MatrixCompressionOp("X", [operation[1]])) # State vector indexing is in little-endian order, the row map is reversed for Q# convention state_prep_params = QSHARP_UTILS.StatePreparation.StatePreparationParams( rowMap=gf2x_operation_results.row_map[::-1], stateVector=statevector_data.tolist(), - expansionOps=expansion_ops, + expansionOps=[op.to_dict() for op in expansion_ops], numQubits=n_qubits, ) @@ -210,8 +208,8 @@ def _qiskit_dense_preparation( statevector = Statevector(statevector_data) qc.append(QiskitStatePreparation(statevector, normalize=False), gf2x_operation_results.row_map) for operation in reversed(gf2x_operation_results.operations): - if operation[0] == "cnot": - # operation[1] should be a tuple for CNOT operations + if operation[0] == "cx": + # operation[1] should be a tuple for CX operations if isinstance(operation[1], tuple): target, control = operation[1] qc.cx(control, target) @@ -260,7 +258,7 @@ def _perform_gf2x(self, bitstrings: list[str], coeffs: np.ndarray) -> tuple["GF2 Logger.debug(f"Total operations: {len(gf2x_operation_results.operations)}") # Log operations by type - Logger.debug(f"CNOT operations: {[op for op in gf2x_operation_results.operations if op[0] == 'cnot']}") + Logger.debug(f"CX operations: {[op for op in gf2x_operation_results.operations if op[0] == 'cx']}") Logger.debug(f"X operations: {[op for op in gf2x_operation_results.operations if op[0] == 'x']}") # Step 3: Create statevector for the reduced matrix @@ -442,7 +440,7 @@ class GF2XEliminationResult: operations: list[tuple[str, int | tuple[int, int]]] """List of operations in the form: - * ('cnot', (target_row, control_row)) for CNOT operations + * ('cx', (target_row, control_row)) for CX operations * ('x', row_index) for X operations on entire rows All indices refer to original matrix positions. @@ -452,12 +450,50 @@ class GF2XEliminationResult: """Rank of the reduced matrix (number of non-zero rows).""" -def gf2x_with_tracking(matrix: np.ndarray) -> GF2XEliminationResult: +@dataclass +class MatrixCompressionOp: + """A single gate in the compressed matrix-encoding circuit. + + Mirrors the Q# ``MatrixCompressionOp`` struct. Use :meth:`to_dict` to + produce a camelCase dict consumable by the Q# bridge. + + Attributes: + name: Gate name (e.g. ``"CX"``, ``"CCX"``, ``"SELECT"``). + qubits: Qubit indices involved in the operation. + control_state: Integer encoding of the control state for multi- + controlled gates. For ``SELECT``/``SELECT_AND``, this stores + the number of address qubits. + lookup_data: Boolean lookup table for ``SELECT`` operations; + empty list for all other gate types. + + """ + + name: str + qubits: list[int] + control_state: int = 0 + lookup_data: list[list[bool]] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a camelCase dict matching the Q# ``MatrixCompressionOp`` struct.""" + return { + "name": self.name, + "qubits": self.qubits, + "controlState": self.control_state, + "lookupData": self.lookup_data, + } + + +def gf2x_with_tracking( + matrix: np.ndarray, + *, + skip_diagonal_reduction: bool = False, + staircase_mode: bool = False, +) -> GF2XEliminationResult: """Perform enhanced GF2+X Gaussian elimination with smart preprocessing and X operations. This function implements a smarter approach to GF2 Gaussian elimination by: - 1. First removing duplicate rows using CNOT operations + 1. First removing duplicate rows using CX operations 2. Removing all-ones rows using X operations 3. Then performing standard Gaussian elimination 4. Performing further reduction if the resulting matrix is diagonal @@ -467,6 +503,17 @@ def gf2x_with_tracking(matrix: np.ndarray) -> GF2XEliminationResult: Args: matrix: shape (m, n), binary (0/1) matrix + skip_diagonal_reduction: If True, skip the optional diagonal-to-upper- + staircase rank reduction (step 4). Binary encoding handles the + identity pivot block natively, so the extra CX + X expansion ops + produced by the diagonal reduction are redundant Cliffords. + staircase_mode: If True, perform forward-only GF2 elimination (REF) + then convert the pivot block directly to upper-staircase form + with minimal CX ops — skipping back-substitution entirely. + This produces a matrix that binary encoding's diagonal path + recognises as ``is_diagonal_reduced``, saving ``r-1`` CX gates + vs. the full RREF + staircase cascade. Implies + ``skip_diagonal_reduction=True``. Returns: A dataclass containing GF2+X elimination results. @@ -498,7 +545,7 @@ def gf2x_with_tracking(matrix: np.ndarray) -> GF2XEliminationResult: # Work on a copy to avoid modifying the input matrix_work = matrix.copy() - # Step 1: Remove duplicate rows using CNOT operations + # Step 1: Remove duplicate rows using CX operations matrix_work, row_map, operations = _remove_duplicate_rows_with_cnot(matrix_work, row_map, operations) # Step 2: Remove all-ones rows using X operations @@ -506,33 +553,52 @@ def gf2x_with_tracking(matrix: np.ndarray) -> GF2XEliminationResult: # Step 3: Perform standard Gaussian elimination on the remaining matrix if matrix_work.shape[0] > 0: # Only if there are rows left - # Convert CNOT operations to the format expected by Gaussian elimination - cnot_ops = [] - for op in operations: - if op[0] == "cnot" and isinstance(op[1], tuple): - cnot_ops.append((op[1][0], op[1][1])) - - # Perform Gaussian elimination - m_current, n_current = matrix_work.shape - matrix_processed, updated_row_map, updated_cnot_ops = _perform_gaussian_elimination( - matrix_work, m_current, n_current, row_map, cnot_ops - ) + if staircase_mode: + # Forward-only elimination → REF → direct staircase fill + matrix_processed, updated_row_map, new_cnot_ops = _perform_gaussian_elimination_forward_only( + matrix_work, row_map, [] + ) - # Update operations list with new CNOT operations from Gaussian elimination - for target, control in updated_cnot_ops[len(cnot_ops) :]: # Only add new operations - operations.append(("cnot", (target, control))) + for target, control in new_cnot_ops: + operations.append(("cx", (target, control))) - # Remove zero rows and update row_map accordingly - matrix_reduced, reduced_row_map, rank = _remove_zero_rows(matrix_processed, updated_row_map) + # Remove zero rows + matrix_reduced, reduced_row_map, rank = _remove_zero_rows(matrix_processed, updated_row_map) - gf2x_results = GF2XEliminationResult( - reduced_matrix=matrix_reduced, row_map=reduced_row_map, col_map=col_map, operations=operations, rank=rank - ) + if rank > 1: + # Convert REF pivot block directly to staircase form + matrix_reduced, reduced_row_map, operations = _ref_to_staircase( + matrix_reduced, reduced_row_map, operations + ) + + gf2x_results = GF2XEliminationResult( + reduced_matrix=matrix_reduced, + row_map=reduced_row_map, + col_map=col_map, + operations=operations, + rank=rank, + ) + else: + matrix_processed, updated_row_map, new_cnot_ops = _perform_gaussian_elimination(matrix_work, row_map, []) + + for target, control in new_cnot_ops: + operations.append(("cx", (target, control))) + + # Remove zero rows and update row_map accordingly + matrix_reduced, reduced_row_map, rank = _remove_zero_rows(matrix_processed, updated_row_map) + + gf2x_results = GF2XEliminationResult( + reduced_matrix=matrix_reduced, + row_map=reduced_row_map, + col_map=col_map, + operations=operations, + rank=rank, + ) - # Step 4: Check for diagonal matrix and apply further reduction if possible - if rank > 1 and _is_diagonal_matrix(matrix_reduced): - Logger.info(f"Detected diagonal matrix with rank {rank}, applying further reduction") - gf2x_results = _reduce_diagonal_matrix(matrix_reduced, reduced_row_map, col_map, operations) + # Step 4: Check for diagonal matrix and apply further reduction if possible + if not skip_diagonal_reduction and rank > 1 and _is_diagonal_matrix(matrix_reduced): + Logger.info(f"Detected diagonal matrix with rank {rank}, applying further reduction") + gf2x_results = _reduce_diagonal_matrix(matrix_reduced, reduced_row_map, col_map, operations) # Log the final reduced matrix rank Logger.info(f"Final reduced matrix rank: {gf2x_results.rank}") @@ -574,56 +640,34 @@ def _remove_duplicate_rows_with_cnot( operations_work = operations.copy() n_rows, _ = matrix_work.shape - rows_to_eliminate = [] + rows_to_eliminate: set[int] = set() - # Find duplicate rows + # Find duplicate rows and XOR them to zero immediately for i in range(n_rows): - # Skip rows that are already marked for elimination if i in rows_to_eliminate: continue - row_i = matrix_work[i] - - # Skip all-zero rows as they don't need CNOT operations - if np.all(row_i == 0): + if not np.any(matrix_work[i]): continue - # Look for duplicates of this row for j in range(i + 1, n_rows): if j in rows_to_eliminate: continue - row_j = matrix_work[j] - - # If rows are identical, eliminate the later one - if np.array_equal(row_i, row_j): - # CNOT(control=i, target=j) will make row j become all zeros - operations_work.append(("cnot", (row_map_work[j], row_map_work[i]))) - rows_to_eliminate.append(j) + if np.array_equal(matrix_work[i], matrix_work[j]): + operations_work.append(("cx", (row_map_work[j], row_map_work[i]))) + matrix_work[j] ^= matrix_work[i] + rows_to_eliminate.add(j) Logger.info( - f"Found duplicate row {j} identical to row {i}, adding CNOT({row_map_work[i]}, {row_map_work[j]})" + f"Found duplicate row {j} identical to row {i}, adding CX({row_map_work[i]}, {row_map_work[j]})" ) - # Apply CNOT operations to eliminate duplicate rows - for op in operations_work: - if op[0] == "cnot" and isinstance(op[1], tuple): - # Find the current positions of the target and control rows - target_orig, control_orig = op[1] - target_current = row_map_work.index(target_orig) - control_current = row_map_work.index(control_orig) - - # Apply CNOT: target row = target row XOR control row - matrix_work[target_current] = matrix_work[target_current] ^ matrix_work[control_current] - - # Remove eliminated rows (which should now be all zeros) + # Remove eliminated rows (now all zeros) if rows_to_eliminate: - Logger.info(f"Eliminating {len(rows_to_eliminate)} duplicate rows: {rows_to_eliminate}") + Logger.info(f"Eliminating {len(rows_to_eliminate)} duplicate rows: {sorted(rows_to_eliminate)}") - # Create mask for rows to keep rows_to_keep = [i for i in range(n_rows) if i not in rows_to_eliminate] - - # Update matrix and row mapping matrix_work = matrix_work[rows_to_keep] row_map_work = [row_map_work[i] for i in rows_to_keep] @@ -684,193 +728,181 @@ def _remove_all_ones_rows_with_x( def _perform_gaussian_elimination( matrix: np.ndarray, - num_rows: int, - num_cols: int, row_map: list[int], cnot_ops: list[tuple[int, int]], ) -> tuple[np.ndarray, list[int], list[tuple[int, int]]]: - """Perform the main GF2 Gaussian elimination steps on a binary matrix. - - This function implements the core algorithm of GF2 Gaussian elimination by iterating through columns, - finding pivot rows, swapping rows when necessary, and eliminating other entries in each column using XOR operations. + """Perform full GF2 Gaussian elimination (forward + back-substitution). Args: - matrix: Binary matrix to reduce (copied, not modified in-place) - num_rows: Number of rows in the matrix - num_cols: Number of columns in the matrix - row_map: Mapping from current to original row indices (copied, not modified) - cnot_ops: List to record CNOT operations (copied, not modified) + matrix: Binary matrix to reduce (copied internally). + row_map: Current-to-original row index mapping (copied internally). + cnot_ops: Existing CNOT operation list (copied internally). Returns: - A tuple containing ``(updated_matrix, updated_row_map, updated_operations)`` + ``(reduced_matrix, updated_row_map, updated_cnot_ops)`` """ matrix_work = matrix.copy() row_map_work = row_map.copy() cnot_ops_work = cnot_ops.copy() + num_rows, num_cols = matrix_work.shape - row = 0 # current row + pivot_row = 0 for col in range(num_cols): - # Find the first row (row >= current) with a 1 in this column - sel = _find_pivot_row(matrix_work, row, num_rows, col) + sel = _find_pivot_row(matrix_work, pivot_row, col) if sel is None: continue - # Swap current row and selected row if needed - if sel != row: - matrix_work[[row, sel], :] = matrix_work[[sel, row], :] - row_map_work[row], row_map_work[sel] = row_map_work[sel], row_map_work[row] + if sel != pivot_row: + matrix_work[[pivot_row, sel]] = matrix_work[[sel, pivot_row]] + row_map_work[pivot_row], row_map_work[sel] = row_map_work[sel], row_map_work[pivot_row] - # Eliminate all other rows (except the pivot row) in this column - matrix_work, cnot_ops_work = _eliminate_column(matrix_work, num_rows, row, col, row_map_work, cnot_ops_work) + _eliminate_column(matrix_work, pivot_row, col, row_map_work, cnot_ops_work) - # Move to next row - row += 1 - if row == num_rows: + pivot_row += 1 + if pivot_row == num_rows: break return matrix_work, row_map_work, cnot_ops_work -def _find_pivot_row(matrix: np.ndarray, row: int, num_rows: int, col: int) -> int | None: - """Find the first row with a 1 in the given column for pivot selection. - - This function searches for a suitable pivot row starting from the current row position downward. - It looks for the first row that has a 1 in the specified column, - which can be used as a pivot for Gaussian elimination. +def _find_pivot_row(matrix: np.ndarray, start_row: int, col: int) -> int | None: + """Find the first row at or below ``start_row`` with a 1 in ``col``. Args: - matrix: Binary matrix to search (read-only, not modified) - row: Starting row index to search from (inclusive) - num_rows: Total number of rows in the matrix - col: Column index to check for pivot candidates + matrix: Binary matrix (read-only). + start_row: First row index to consider (inclusive). + col: Column to search. Returns: - Index of the first row with a 1 in the column, or None if no suitable pivot is found in the remaining rows. - - Note: - This function only reads the matrix and does not modify any arguments. - It returns None when no pivot can be found, indicating the column should be skipped. + Row index of the first 1-entry, or ``None`` if the column is + all-zero from ``start_row`` downward. """ - for r in range(row, num_rows): - if matrix[r, col]: - return r - return None + candidates = np.flatnonzero(matrix[start_row:, col]) + return start_row + int(candidates[0]) if candidates.size > 0 else None def _eliminate_column( matrix: np.ndarray, - num_rows: int, pivot_row: int, col: int, row_map: list[int], cnot_ops: list[tuple[int, int]], -) -> tuple[np.ndarray, list[tuple[int, int]]]: - """Eliminate all other rows in the given column using XOR operations. +) -> None: + """Eliminate all other rows in ``col`` using XOR with the pivot row. - This function performs the elimination step of GF2 Gaussian elimination - by XORing the pivot row with all other rows that have a 1 in the current column. + Modifies ``matrix`` and ``cnot_ops`` **in place**. Args: - matrix: Binary matrix to modify (copied, not modified in-place) - num_rows: Number of rows in the matrix - pivot_row: Index of the pivot row (remains unchanged) - col: Column index to eliminate - row_map: Mapping from current to original row indices (read-only) - cnot_ops: List to record CNOT operations (copied, not modified) + matrix: Binary matrix (modified in place). + pivot_row: Index of the pivot row (unchanged). + col: Column to eliminate. + row_map: Current-to-original row index mapping (read-only). + cnot_ops: Destination list for recorded CNOT operations. - Returns: - tuple[np.ndarray, list[tuple[int, int]]]: Tuple containing: + """ + targets = np.flatnonzero(matrix[:, col]) + targets = targets[targets != pivot_row] + for r in targets: + matrix[r] ^= matrix[pivot_row] + cnot_ops.append((row_map[r], row_map[pivot_row])) - * ``updated_matrix``: Matrix after column elimination. - * ``updated_cnot_ops``: Updated list of CNOT operations. - """ - matrix_work = matrix.copy() - cnot_ops_work = cnot_ops.copy() +def _remove_zero_rows(matrix: np.ndarray, row_map: list[int]) -> tuple[np.ndarray, list[int], int]: + """Remove all-zero rows from the matrix and update the row mapping. - for r in range(num_rows): - if r != pivot_row and matrix_work[r, col]: - matrix_work[r, :] ^= matrix_work[pivot_row, :] - # Record CNOT operation using original matrix indices - cnot_ops_work.append((row_map[r], row_map[pivot_row])) + Args: + matrix: Binary matrix (read-only). + row_map: Current-to-original row index mapping (read-only). - return matrix_work, cnot_ops_work + Returns: + ``(matrix_reduced, reduced_row_map, rank)`` where ``rank`` is the + number of retained (non-zero) rows. + """ + non_zero_indices = np.flatnonzero(np.any(matrix, axis=1)) + return ( + matrix[non_zero_indices], + [row_map[i] for i in non_zero_indices], + int(non_zero_indices.size), + ) -def _remove_zero_rows(matrix: np.ndarray, row_map: list[int]) -> tuple[np.ndarray, list[int], int]: - """Remove zero rows from the matrix and update row mapping. - This function creates a new matrix containing only the non-zero rows from the input matrix, - along with an updated row mapping that tracks which original rows correspond to the rows in the reduced matrix. +def _perform_gaussian_elimination_forward_only( + matrix: np.ndarray, + row_map: list[int], + cnot_ops: list[tuple[int, int]], +) -> tuple[np.ndarray, list[int], list[tuple[int, int]]]: + """Perform forward-only GF2 Gaussian elimination (no back-substitution). + + Produces an upper-triangular (row echelon form) matrix rather than RREF. + Back-substitution is skipped so that binary encoding's staircase + conversion can be applied directly to the REF pivot block. Args: - matrix: Binary matrix to process (read-only, not modified) - row_map: Current mapping from matrix rows to original indices (read-only) + matrix: Binary matrix to reduce (copied internally). + row_map: Current-to-original row index mapping (copied internally). + cnot_ops: Existing CNOT operation list (copied internally). Returns: - tuple[np.ndarray, list[int], int]: Tuple containing: + ``(reduced_matrix, updated_row_map, updated_cnot_ops)`` - * ``matrix_reduced``: New matrix with only non-zero rows. - * ``reduced_row_map``: Updated mapping from reduced matrix rows to original indices. - * ``rank``: Number of non-zero rows (matrix rank). + """ + matrix_work = matrix.copy() + row_map_work = row_map.copy() + cnot_ops_work = cnot_ops.copy() + num_rows, num_cols = matrix_work.shape - Note: - This function does not modify its input arguments. It creates and returns - new objects containing only the non-zero rows and their corresponding mappings. + pivot_row = 0 + for col in range(num_cols): + sel = _find_pivot_row(matrix_work, pivot_row, col) + if sel is None: + continue - """ - n_rows, _ = matrix.shape - non_zero_rows = [] - reduced_row_map = [] + if sel != pivot_row: + matrix_work[[pivot_row, sel]] = matrix_work[[sel, pivot_row]] + row_map_work[pivot_row], row_map_work[sel] = row_map_work[sel], row_map_work[pivot_row] - for i in range(n_rows): - if not np.all(matrix[i, :] == 0): # Keep non-zero rows - non_zero_rows.append(i) - reduced_row_map.append(row_map[i]) + # Eliminate only rows BELOW the pivot (forward elimination only) + below = np.flatnonzero(matrix_work[pivot_row + 1 :, col]) + pivot_row + 1 + for r in below: + matrix_work[r] ^= matrix_work[pivot_row] + cnot_ops_work.append((row_map_work[r], row_map_work[pivot_row])) - # Extract only non-zero rows - matrix_reduced = matrix[non_zero_rows, :] - rank = len(non_zero_rows) + pivot_row += 1 + if pivot_row == num_rows: + break - return matrix_reduced, reduced_row_map, rank + return matrix_work, row_map_work, cnot_ops_work -def _reduce_diagonal_matrix( +def _ref_to_staircase( matrix: np.ndarray, row_map: list[int], - col_map: list[int], operations: list[tuple[str, int | tuple[int, int]]], -) -> GF2XEliminationResult: - """Further reduce a diagonal matrix using CNOT and X operations. +) -> tuple[np.ndarray, list[int], list[tuple[str, int | tuple[int, int]]]]: + """Convert a REF (upper-triangular) pivot block to upper-staircase form. + + For each above-diagonal entry ``(i, pivot_col_j)`` where ``j > i``: - This function handles the special case where the matrix is diagonal - (square matrix with 1s on diagonal and 0s elsewhere). It applies - sequential CNOT operations to create an all-ones row, then uses - an X operation to eliminate it, reducing the rank by 1. + * If the entry is already 1, it's correct — do nothing. + * If the entry is 0, apply CX(control=row_j, target=row_i) to set it to 1. - Procedure: + Processing columns left-to-right guarantees that side effects on later + columns are absorbed when we reach them. - 1. Apply CNOT(i, i+1) sequentially for i = 0 to rank-2 - 2. This makes the last row (rank-1) become all 1s - 3. Apply X on the last row to make it all 0s - 4. Remove the zero row to reduce rank by 1 + After this transform the pivot sub-matrix equals ``np.triu(ones(r, r))`` + which binary encoding's ``_is_diagonal_reduction_shape`` recognises, + allowing it to skip its own CX cascade. Args: - matrix: Diagonal binary matrix to reduce - row_map: Current row mapping to original indices - col_map: Column mapping to original indices (unchanged) - operations: List of operations performed so far + matrix: REF binary matrix (rank x n_cols). + row_map: Current row-to-original-qubit mapping. + operations: Existing operations list to extend. Returns: - A tuple containing: - - * matrix_reduced: Further reduced matrix with rank decreased by 1 - * reduced_row_map: Updated row mapping (last row removed) - * col_map: Unchanged column mapping - * updated_operations: Operations list with new CNOT and X operations - * new_rank: Original rank - 1 + ``(matrix, row_map, operations)`` — updated in place. """ matrix_work = matrix.copy() @@ -879,121 +911,103 @@ def _reduce_diagonal_matrix( rank = matrix_work.shape[0] - # Verify this is actually a diagonal matrix - if not _is_diagonal_matrix(matrix_work): - Logger.warn("Matrix is not diagonal, skipping diagonal reduction") - return GF2XEliminationResult( - reduced_matrix=matrix_work, row_map=row_map_work, col_map=col_map, operations=operations_work, rank=rank - ) + # Identify pivot columns + pivot_cols: list[int] = [] + for r in range(rank): + nz = np.flatnonzero(matrix_work[r]) + if nz.size > 0: + pivot_cols.append(int(nz[0])) + + # Fill above-diagonal entries in pivot columns to reach staircase form + for j_idx in range(1, len(pivot_cols)): + pc = pivot_cols[j_idx] + for i_idx in range(j_idx): + if not matrix_work[i_idx, pc]: + # Need to set this entry to 1: CX(control=row_j, target=row_i) + matrix_work[i_idx] ^= matrix_work[j_idx] + operations_work.append(("cx", (row_map_work[i_idx], row_map_work[j_idx]))) - Logger.info(f"Applying diagonal matrix reduction on {rank}x{rank} matrix") + return matrix_work, row_map_work, operations_work - # Step 1: Apply sequential CNOT operations CNOT(i, i+1) for i = 0 to rank-2 - for i in range(rank - 1): - control_idx = i - target_idx = i + 1 - - # Record CNOT operation using original row indices - operations_work.append( - ( - "cnot", - (row_map_work[target_idx], row_map_work[control_idx]), - ) - ) - # Apply CNOT: target row = target row XOR control row - matrix_work[target_idx] = matrix_work[target_idx] ^ matrix_work[control_idx] +def _reduce_diagonal_matrix( + matrix: np.ndarray, + row_map: list[int], + col_map: list[int], + operations: list[tuple[str, int | tuple[int, int]]], +) -> GF2XEliminationResult: + """Reduce a diagonal (identity) matrix by one rank via CX cascade + X. - Logger.info(f"Applied CNOT({row_map_work[control_idx]}, {row_map_work[target_idx]})") + Applies CX(i, i+1) for i = 0…rank-2, making the last row all-ones, + then X on the last row to zero it, and finally removes that row. - # After all CNOTs, the last row should be all 1s - last_row = rank - 1 - Logger.info(f"Last row after CNOTs: {matrix_work[last_row]}") + The caller is responsible for verifying ``_is_diagonal_matrix`` first. - # Step 2: Apply X operation on the last row to make it all 0s - operations_work.append(("x", row_map_work[last_row])) - matrix_work[last_row] = np.zeros(matrix_work.shape[1], dtype=matrix_work.dtype) + Args: + matrix: Diagonal binary matrix to reduce. + row_map: Current row mapping to original indices. + col_map: Column mapping (passed through unchanged). + operations: Operations list to extend. - Logger.info(f"Applied X operation on row {row_map_work[last_row]}") + Returns: + :class:`GF2XEliminationResult` with rank decremented by 1. - # Step 3: Remove the last row (which is now all zeros) - matrix_reduced = matrix_work[:-1, :] # Remove last row - reduced_row_map = row_map_work[:-1] # Remove last row mapping - new_rank = rank - 1 + """ + matrix_work = matrix.copy() + row_map_work = row_map.copy() + operations_work = operations.copy() + rank = matrix_work.shape[0] + + Logger.info(f"Applying diagonal matrix reduction on {rank}x{matrix_work.shape[1]} matrix") - Logger.info(f"Diagonal reduction complete: rank reduced from {rank} to {new_rank}") + # Sequential CX(i, i+1) accumulates all 1s into the last row + for i in range(rank - 1): + operations_work.append(("cx", (row_map_work[i + 1], row_map_work[i]))) + matrix_work[i + 1] ^= matrix_work[i] + + # X on the all-ones last row zeroes it + operations_work.append(("x", row_map_work[rank - 1])) + + Logger.info(f"Diagonal reduction complete: rank reduced from {rank} to {rank - 1}") return GF2XEliminationResult( - reduced_matrix=matrix_reduced, - row_map=reduced_row_map, + reduced_matrix=matrix_work[:-1], + row_map=row_map_work[:-1], col_map=col_map, operations=operations_work, - rank=new_rank, + rank=rank - 1, ) def _is_diagonal_matrix(matrix: np.ndarray) -> bool: - """Check if a binary matrix is diagonal and safe for a further rank reduction. - - The diagonal reduction optimization is mathematically valid in two scenarios: + """Check if a binary matrix is diagonal and safe for rank reduction. - 1. True diagonal matrix: Square matrix with 1s on diagonal, 0s elsewhere - 2. Pseudo-diagonal: Rectangular matrix where: - * The square part (min(rows,cols) x min(rows,cols)) is diagonal - * ALL remaining columns are all 1s - * We have an odd number of rows + Two accepted shapes: - The rank reduction works by applying sequential CNOTs and an X operation, - which is only valid when these specific structural conditions are met. - We also require rank > 1 since rank 1 matrices are already minimal. + 1. **Square identity**: ``matrix == np.eye(r)``. + 2. **Pseudo-diagonal** (more columns than rows, odd row count): + the leading ``r x r`` block is identity and every extra column + is all-ones. Args: - matrix: Binary matrix to check + matrix: Binary matrix to check. Returns: - True if matrix is diagonal and safe for rank reduction, False otherwise + ``True`` if the matrix matches one of the accepted shapes. """ - # Check basic requirements - if matrix.ndim != 2 or matrix.shape[0] <= 1 or not np.array_equal(matrix & 1, matrix): + if matrix.ndim != 2 or matrix.shape[0] <= 1: return False num_rows, num_cols = matrix.shape + identity = np.eye(num_rows, dtype=matrix.dtype) - # Scenario 1: True diagonal matrix (square) if num_rows == num_cols: - is_diagonal = True - for row_idx in range(num_rows): - for col_idx in range(num_rows): - expected_value = 1 if row_idx == col_idx else 0 - if matrix[row_idx, col_idx] != expected_value: - is_diagonal = False - break - if not is_diagonal: - break - if is_diagonal: - return True - - # Scenario 2: Pseudo-diagonal (rectangular with more columns than rows) - # ONLY valid when: remaining columns are ALL 1s AND odd number of rows - elif num_cols > num_rows and num_rows % 2 == 1: - # Check the square part is diagonal - square_part = matrix[:num_rows, :num_rows] - is_square_diagonal = True - for row_idx in range(num_rows): - for col_idx in range(num_rows): - expected_value = 1 if row_idx == col_idx else 0 - if square_part[row_idx, col_idx] != expected_value: - is_square_diagonal = False - break - if not is_square_diagonal: - break - - # If square part is diagonal, check remaining columns are all 1s - if is_square_diagonal: - remaining_columns = matrix[:, num_rows:] - if np.all(remaining_columns == 1): - return True - - # All other cases: not safe for diagonal reduction - return False + return bool(np.array_equal(matrix, identity)) + + return ( + num_cols > num_rows + and num_rows % 2 == 1 + and bool(np.array_equal(matrix[:, :num_rows], identity)) + and bool(np.all(matrix[:, num_rows:] == 1)) + ) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py new file mode 100644 index 000000000..4947c331a --- /dev/null +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -0,0 +1,303 @@ +"""Sparse isometry with binary encoding for quantum state preparation. + +This module implements a state preparation algorithm that combines GF2+X +elimination with batched binary encoding. Instead of delegating the reduced +subspace to a dense state preparation routine (as the base +:class:`SparseIsometryGF2XStatePreparation` does), this algorithm feeds the +RREF matrix directly into the binary-encoding solver which synthesises the +full circuit using batched Toffoli gates and Partial Unary Iteration (PUI) +lookup blocks. + +The approach is particularly effective for wavefunctions with many +determinants whose binary matrix has favourable sparsity structure, +since the entire expansion is expressed in terms of CX, Toffoli, and +lookup gates with no intermediate dense state preparation. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from typing import Any + +import numpy as np +import qdk + +from qdk_chemistry.data import Circuit, Wavefunction +from qdk_chemistry.data.circuit import QsharpFactoryData +from qdk_chemistry.utils import Logger +from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer +from qdk_chemistry.utils.qsharp import QSHARP_UTILS + +from .sparse_isometry import ( + GF2XEliminationResult, + MatrixCompressionOp, + SparseIsometryGF2XStatePreparation, + SparseIsometryGF2XStatePreparationSettings, + gf2x_with_tracking, +) + + +class SparseIsometryBinaryEncodingSettings(SparseIsometryGF2XStatePreparationSettings): + """Settings for SparseIsometryBinaryEncodingStatePreparation.""" + + def __init__(self): + """Initialize with parent defaults plus binary-encoding controls.""" + super().__init__() + self._set_default( + "include_negative_controls", + "bool", + True, + "Include both positive and negative fixed controls in PUI construction.", + ) + self._set_default( + "measurement_based_uncompute", + "bool", + False, + "Use measurement-based AND uncomputation in PUI blocks.", + ) + + +class SparseIsometryBinaryEncodingStatePreparation(SparseIsometryGF2XStatePreparation): + """State preparation using sparse isometry with binary encoding. + + This class extends :class:`SparseIsometryGF2XStatePreparation` by replacing + the dense state preparation step with a binary-encoding circuit synthesiser. + After GF2+X elimination produces a reduced RREF matrix, the binary-encoding + synthesiser (:class:`~qdk_chemistry.algorithms.state_preparation.binary_encoding.BinaryEncodingSynthesizer`) + compresses the matrix into an efficient circuit using: + + 1. Stage 1 — diagonal (unary-to-binary) encoding of pivot columns + 2. Stage 2 — non-pivot column processing with batched PUI lookup blocks + + The resulting circuit consists entirely of CX, Toffoli, X, SWAP, and + PUI-lookup gates — no dense state preparation is used. + + Key References: + + * Sparse isometry: Malvetti, Iten, and Colbeck (arXiv:2006.00016) :cite:`Malvetti2021` + """ + + def __init__(self) -> None: + """Initialize the SparseIsometryBinaryEncodingStatePreparation.""" + Logger.trace_entering() + super().__init__() + self._settings = SparseIsometryBinaryEncodingSettings() + + def _run_impl(self, wavefunction: Wavefunction) -> Circuit: + """Prepare a quantum circuit using GF2+X elimination followed by binary encoding. + + Args: + wavefunction: The target wavefunction to prepare. + + Returns: + A Circuit object containing the quantum circuit. + + """ + Logger.trace_entering() + + # Active Space Consistency Check (same as parent) + alpha_indices, beta_indices = wavefunction.get_orbitals().get_active_space_indices() + if alpha_indices != beta_indices: + raise ValueError( + f"Active space contains {len(alpha_indices)} alpha orbitals and " + f"{len(beta_indices)} beta orbitals. Asymmetric active spaces for " + "alpha and beta orbitals are not supported for state preparation." + ) + + coeffs = wavefunction.get_coefficients() + dets = wavefunction.get_active_determinants() + num_orbitals = len(wavefunction.get_orbitals().get_active_space_indices()[0]) + bitstrings = [] + for det in dets: + alpha_str, beta_str = det.to_binary_strings(num_orbitals) + bitstrings.append(beta_str[::-1] + alpha_str[::-1]) + + if len(bitstrings) == 1: + Logger.info("After filtering, only 1 determinant remains, using single reference state preparation") + return self._prepare_single_reference_state(bitstrings[0]) + + n_qubits = len(bitstrings[0]) + Logger.debug(f"Using {len(bitstrings)} determinants for state preparation") + + # Step 1: GF2+X elimination — skip the diagonal reduction because + # binary encoding's stage-1 handles the identity pivot block natively; + # the extra CX + X ops from diagonal reduction would be redundant. + bitstring_matrix = self._bitstrings_to_binary_matrix(bitstrings) + gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, staircase_mode=True) + + # Step 2: Binary encoding on the reduced RREF matrix + binary_ops, num_ancilla, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) + + # Step 2b: Build compressed statevector reindexed by the bijection. + # The bijection maps (dense_val, orig_col) where orig_col is the + # determinant index and dense_val is the binary-register label. + compressed_sv = np.zeros(2**dense_size, dtype=float) + for dense_val, orig_col in bijection: + if orig_col < len(coeffs): + compressed_sv[dense_val] = coeffs[orig_col] + norm = np.linalg.norm(compressed_sv) + if norm > 0: + compressed_sv /= norm + + # The dense register consists of the first dense_size rows of the + # tableau, which map to the first dense_size entries of row_map. + dense_row_map = gf2x_result.row_map[:dense_size] + + # Step 3: Build expansion operations from GF2+X elimination + expansion_ops: list[MatrixCompressionOp] = [] + for operation in reversed(gf2x_result.operations): + if operation[0] in ("cx", "cnot"): + if isinstance(operation[1], tuple): + target, control = operation[1] + expansion_ops.append(MatrixCompressionOp("CX", [control, target])) + elif operation[0] == "x" and isinstance(operation[1], int): + expansion_ops.append(MatrixCompressionOp("X", [operation[1]])) + + # Step 4: Pre-process binary-encoding ops into MatrixCompressionOp instances + encoded_ops = _encode_gf2x_ops_for_qs(binary_ops) + + # Step 4b: Elide redundant CX pair at the boundary. + # Binary encoding's unary staircase always starts with CX(row_map[rank-1], row_map[rank-2]) + # which, after reversal, becomes the LAST encoded_op. + # GF2+X back-substitution often ends with the same CX (clearing the + # entry above the last pivot), which becomes the FIRST expansion_op. + # Since CX is self-inverse, the pair cancels — remove both. + if ( + encoded_ops + and expansion_ops + and encoded_ops[-1].name == "CX" + and expansion_ops[0].name == "CX" + and encoded_ops[-1].qubits == expansion_ops[0].qubits + ): + Logger.debug(f"Eliding redundant boundary CX pair on qubits {encoded_ops[-1].qubits}") + encoded_ops.pop() + expansion_ops.pop(0) + + # Build circuit using QDK Q# factory with binary-encoding entry point + # dense_val from the bijection uses row 0 = MSB (_bits_to_int is MSB-first). + # PreparePureStateD treats qubits[0] as MSB, so pass dense_row_map + # as-is (row 0 first) — do NOT reverse like the parent sparse isometry + # (which uses the opposite convention: row rank-1 = MSB). + state_prep_params = qdk.code.BinaryEncodingStatePreparationParams( + rowMap=list(dense_row_map), + stateVector=compressed_sv.tolist(), + expansionOps=[op.to_dict() for op in expansion_ops], + binaryEncodingOps=[op.to_dict() for op in encoded_ops], + numQubits=n_qubits, + numAncilla=num_ancilla, + ) + + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationCircuit, + parameter=vars(state_prep_params), + ) + + Logger.info( + f"Binary encoding produced {len(binary_ops)} operations ({len(encoded_ops)} encoded) " + f"using {num_ancilla} ancillae for {n_qubits}-qubit system with {len(bitstrings)} determinants" + ) + + return Circuit( + qsharp_factory=qsharp_factory, + encoding="jordan-wigner", + ) + + def _perform_binary_encoding( + self, gf2x_result: GF2XEliminationResult, n_qubits: int + ) -> tuple[list[tuple[str, Any]], int, list[tuple[int, int]], int]: + """Run binary-encoding synthesis on the reduced RREF matrix. + + Args: + gf2x_result: Result from GF2+X elimination containing the reduced matrix. + n_qubits: Total number of qubits in the original space. + + Returns: + Tuple of ``(gf2x_ops, num_ancilla, bijection, dense_size)``: + + - ``gf2x_ops``: gate operations from the binary-encoding solver. + - ``num_ancilla``: number of ancilla qubits consumed. + - ``bijection``: list of ``(dense_val, orig_col)`` mapping each + original matrix column to its compressed binary-register label. + - ``dense_size``: number of qubits in the compressed dense register. + + """ + include_negative_controls = self._settings.get("include_negative_controls") + + Logger.debug( + f"Binary encoding input: {gf2x_result.reduced_matrix.shape} matrix, " + f"include_negative_controls={include_negative_controls}" + ) + + synthesizer = BinaryEncodingSynthesizer.from_matrix( + gf2x_result.reduced_matrix, + include_negative_controls=include_negative_controls, + measurement_based_uncompute=self._settings.get("measurement_based_uncompute"), + ) + + gf2x_ops, num_ancilla = synthesizer.to_gf2x_operations( + num_local_qubits=n_qubits, + active_qubit_indices=gf2x_result.row_map, + ancilla_start=n_qubits, + ) + + Logger.debug( + f"Binary encoding output: {len(gf2x_ops)} ops, " + f"{num_ancilla} ancillae, bijection size {len(synthesizer.bijection)}, " + f"dense_size {synthesizer.dense_size}" + ) + + return gf2x_ops, num_ancilla, synthesizer.bijection, synthesizer.dense_size + + def name(self) -> str: + """Return the algorithm identifier string.""" + return "sparse_isometry_binary_encoding" + + +def _encode_gf2x_ops_for_qs( + operations: list[tuple[str, Any]], +) -> list[MatrixCompressionOp]: + """Pre-process GF2+X operations into :class:`MatrixCompressionOp` instances for Q#. + + The resulting list is already in *reversed* order so that Q# can iterate + forward. + + Args: + operations: Sequence of ``(op_name, op_args)`` tuples as produced by + :meth:`BinaryEncodingSynthesizer.to_gf2x_operations`. + + Returns: + List of :class:`MatrixCompressionOp` ready to be serialised for Q#. + + """ + ops: list[MatrixCompressionOp] = [] + + for op_name, op_args in reversed(operations): + if op_name == "x": + ops.append(MatrixCompressionOp("X", [int(op_args)])) + + elif op_name == "cx": + ops.append(MatrixCompressionOp("CX", [int(op_args[0]), int(op_args[1])])) + + elif op_name == "swap": + ops.append(MatrixCompressionOp("SWAP", [int(op_args[0]), int(op_args[1])])) + + elif op_name == "ccx": + target, ctrl1, ctrl2 = op_args + ops.append(MatrixCompressionOp("CCX", [int(ctrl1), int(ctrl2), int(target)])) + + elif op_name in ("select", "select_and"): + data_table, addr_qubits, dat_qubits = op_args + qubits = [int(q) for q in addr_qubits] + [int(q) for q in dat_qubits] + qs_name = "SELECT_AND" if op_name == "select_and" else "SELECT" + ops.append( + MatrixCompressionOp( + qs_name, + qubits, + control_state=len(addr_qubits), + lookup_data=data_table, + ) + ) + + return ops diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py new file mode 100644 index 000000000..757485907 --- /dev/null +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -0,0 +1,1114 @@ +"""Binary encoding circuit synthesiser for RREF matrices. + +This module implements the "binary encoding" step of the GF2+X pipeline: +given a reduced binary matrix (in RREF or upper-staircase diagonal form), +it synthesises a circuit that compresses the sparse rows into a dense +binary-counter register using batched Toffoli gates and Partial Unary +Iteration (PUI) lookup blocks. + +Public API +---------- +:class:`BinaryEncodingSynthesizer` + Main facade. Use :meth:`BinaryEncodingSynthesizer.from_matrix` to + create a solved instance, then call + :meth:`~BinaryEncodingSynthesizer.to_gf2x_operations` to export + the resulting gate sequence. +:class:`RrefTableau` + Mutable binary tableau with RREF validation and gate-level updates. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from __future__ import annotations + +import math +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Any, cast + +import numpy as np + +from qdk_chemistry.utils import Logger + +if TYPE_CHECKING: + from collections.abc import Iterable + + +__all__ = [ + "BinaryEncodingSynthesizer", + "MatrixCompressionType", + "NotRrefError", + "RrefTableau", +] + + +def _dense_qubits_size(num_cols: int) -> int: + """Return the dense-register width required to index ``num_cols`` columns.""" + return 1 if num_cols < 2 else math.ceil(math.log2(num_cols)) + + +def _int_to_bits(val: int, nbits: int) -> list[bool]: + """Convert an integer to a fixed-width MSB-first bit sequence.""" + return [bool((val >> i) & 1) for i in range(nbits - 1, -1, -1)] + + +def _bits_to_int(bits: Iterable[int | bool]) -> int: + """Convert an MSB-first bit sequence to integer.""" + return sum(int(b) << i for i, b in enumerate(reversed(list(bits)))) + + +class NotRrefError(ValueError): + """Raised when a matrix is not in reduced row echelon form (RREF).""" + + +class MatrixCompressionType(Enum): + """Operations types recorded during matrix compression.""" + + CX = "cx" + SWAP = "swap" + TOFFOLI = "toffoli" + PUI_BLOCK = "pui_block" + X = "x" + + +def _is_diagonal_reduction_shape(data: np.ndarray) -> bool: + """Return True when the pivot sub-matrix is already upper-staircase. + + The filled-REF pattern produced by ``_ref_to_staircase`` has pivot + columns forming ``np.triu(ones(r, r))`` — upper triangular with 1s on + and above the diagonal. When this is detected, binary encoding can + skip its own CX cascade in ``_apply_unary_staircase`` (only X on the + first row is needed). + """ + row_norms = np.any(data, axis=1) + effective_rows = int(row_norms.sum()) if row_norms.any() else 0 + if effective_rows == 0: + return False + + # Identify pivot columns (leftmost 1 in each non-zero row) + pivot_cols: list[int] = [] + for r in range(effective_rows): + nz = np.flatnonzero(data[r]) + if nz.size == 0: + return False + pivot_cols.append(int(nz[0])) + + # Check that pivot sub-matrix is upper-triangular with all 1s + pivot_submatrix = data[np.ix_(range(effective_rows), pivot_cols)] + expected = np.triu(np.ones((effective_rows, effective_rows), dtype=np.int8)) + return bool(np.array_equal(pivot_submatrix, expected)) + + +def _check_rref(data: np.ndarray) -> None: + """Validate that a binary matrix is in reduced row echelon form.""" + num_rows, _ = data.shape + prev_pivot = -1 + found_zero_row = False + for row in range(num_rows): + nz = np.flatnonzero(data[row]) + if nz.size == 0: + found_zero_row = True + continue + if found_zero_row: + raise NotRrefError(f"Non-zero row {row} appears after an all-zero row") + + pivot_col = int(nz[0]) + if pivot_col <= prev_pivot: + raise NotRrefError( + f"Pivot at row {row}, col {pivot_col} is not strictly to the right of previous pivot col {prev_pivot}" + ) + col_sum = int(data[:, pivot_col].sum()) + if col_sum != 1: + raise NotRrefError(f"Pivot column {pivot_col} has {col_sum} non-zero entries (expected 1)") + prev_pivot = pivot_col + + +class RrefTableau: + """Binary tableau for the batched sparse-isometry algorithm. + + The input matrix must be in reduced row echelon form (RREF) or + upper-staircase diagonal-reduction shape. + + The tableau supports in-place updates via the compression operations. + """ + + def __init__(self, data: np.ndarray): + """Create a tableau from a binary matrix and validate its shape. + + Args: + data: Binary matrix with rows as qubits and columns as determinant + basis states. Values are coerced to ``np.int8``. + + Raises: + NotRrefError: If ``data`` is neither in RREF nor in the accepted + upper-staircase diagonal-reduction form. + AssertionError: If the matrix rank/size assumptions required by + the algorithm are violated. + + """ + self.data = np.asarray(data, dtype=np.int8) + assert self.data.ndim == 2 + + self.is_diagonal_reduced = _is_diagonal_reduction_shape(self.data) + if not self.is_diagonal_reduced: + _check_rref(self.data) + + self.num_rows, self.num_cols = self.data.shape + self.dense_size = _dense_qubits_size(self.num_cols) + assert self.dense_size < self.num_rows + + self._tmp_row = np.zeros(self.num_cols, dtype=np.int8) + self.pivots = self.identify_rref_pivots() + + Logger.debug(f"Tableau shape: {self.data.shape}, dense size: {self.dense_size}, pivots: {self.pivots}") + + def get(self, row: int, col: int) -> bool: + """Return the value at ``(row, col)``.""" + return bool(self.data[row, col]) + + def get_col(self, col: int) -> np.ndarray: + """Return column *col* as a 1-D array.""" + return self.data[:, col] + + def row_is_zero(self, row: int) -> bool: + """Return True if *row* is all zeros.""" + return not np.any(self.data[row]) + + def identify_rref_pivots(self) -> list[tuple[int, int]]: + """Find pivot positions using vectorized operations.""" + row_indices, col_indices = np.nonzero(self.data) + _, first_occurrences = np.unique(row_indices, return_index=True) + return list( + zip( + row_indices[first_occurrences].tolist(), + col_indices[first_occurrences].tolist(), + strict=True, + ) + ) + + def cx(self, control: int, target: int): + """Apply CX: ``target ^= control``.""" + self.data[target] ^= self.data[control] + + def swap(self, a: int, b: int): + """Swap rows *a* and *b*.""" + self.data[[a, b]] = self.data[[b, a]] + + def x(self, row: int): + """Apply bit-flip (X) to every entry in *row*.""" + self.data[row] ^= 1 + + def permute_columns(self, col_order: list[int]): + """Reorder tableau columns and refresh derived metadata. + + Args: + col_order: New-to-old index mapping used to permute columns. + + Notes: + This recomputes ``num_cols``, resets the temporary PUI mask buffer, + and refreshes cached pivot positions. + + """ + self.data = self.data[:, col_order].copy() + self.num_cols = self.data.shape[1] + self._tmp_row = np.zeros(self.num_cols, dtype=np.int8) + self.pivots = self.identify_rref_pivots() + + def toffoli(self, target: int, ctrl0: tuple[int, bool], ctrl1: tuple[int, bool]): + """Apply a two-control conditional XOR into ``target``. + + Args: + target: Target-row index. + ctrl0: Pair ``(row, value)`` for first control; when ``value`` is + ``False``, the negated control is used. + ctrl1: Pair ``(row, value)`` for second control; when ``value`` is + ``False``, the negated control is used. + + """ + c0, v0 = ctrl0 + c1, v1 = ctrl1 + m0 = self.data[c0] if v0 else (1 - self.data[c0]) + m1 = self.data[c1] if v1 else (1 - self.data[c1]) + self.data[target] ^= m0 & m1 + + def _and_controls_into_mask(self, mask: np.ndarray, controls: list[tuple[int, bool]]) -> None: + """Constrain a mask by conjunction over signed controls. + + Args: + mask: Mutable bit-mask updated in place. + controls: Control predicates ``(row, required_value)``. + + """ + for row, val in controls: + mask &= self.data[row] if val else (1 - self.data[row]) + + def toffoli_pui_fixed(self, controls: list[tuple[int, bool]]): + """Initialize shared PUI mask from fixed controls. + + Args: + controls: Controls common to every target in one PUI block. + + """ + self._tmp_row[:] = 1 + self._and_controls_into_mask(self._tmp_row, controls) + + def toffoli_pui_rest(self, target_row_offset: int, controls: list[tuple[int, bool]]): + """Apply one branch of a prepared PUI update. + + Args: + target_row_offset: Offset from dense/sparse boundary to the target + sparse row. + controls: Additional branch-specific controls. + + """ + target = self.dense_size + target_row_offset + mask = self._tmp_row.copy() + self._and_controls_into_mask(mask, controls) + self.data[target] ^= mask + + +@dataclass +class _BatchElement: + """Internal tracking structure for one element within a synthesis batch.""" + + col: int | None + dense_content: int + + +class BinaryEncodingSynthesizer: + """Synthesise a circuit from a binary RREF tableau using batched sparse isometry. + + The synthesiser executes a two-stage algorithm: + + * **Stage 1 — diagonal encoding**: converts the identity pivot block into + a compact binary-counter register via a unary-to-binary ladder. + * **Stage 2 — non-pivot processing**: encodes remaining columns using + batched Toffoli gates and Partial Unary Iteration (PUI) lookup blocks. + + Typical usage:: + + synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + ops, n_anc = synth.to_gf2x_operations(num_local_qubits=n) + + Or, for more control:: + + synth = BinaryEncodingSynthesizer(RrefTableau(matrix)) + synth.run() + ops, n_anc = synth.to_gf2x_operations(num_local_qubits=n) + """ + + def __init__( + self, + tableau: RrefTableau, + *, + include_negative_controls: bool = True, + measurement_based_uncompute: bool = False, + ): + """Construct solver state for a validated tableau. + + Args: + tableau: Mutable tableau to transform during synthesis. + include_negative_controls: If True, include both positive and + negative (0-valued) fixed controls in PUI blocks. If False, + only positive (1-valued) controls are emitted. + measurement_based_uncompute: If True, emit ``select_and`` ops + that use measurement-based AND uncomputation (requires + Adaptive_RI target profile or higher). + + """ + self.tableau = tableau + self.include_negative_controls = include_negative_controls + self.measurement_based_uncompute = measurement_based_uncompute + + self.batch: list[_BatchElement] = [] + self.batch_index: int = 0 + + self.circuit: list[tuple[MatrixCompressionType, tuple[Any, ...]]] = [] + self.bijection: list[tuple[int, int]] = [] + self.bad_element_count: int = 0 + + @property + def dense_size(self) -> int: + """Return the number of dense-register rows.""" + return self.tableau.dense_size + + @classmethod + def from_matrix( + cls, + matrix: np.ndarray, + *, + include_negative_controls: bool = True, + measurement_based_uncompute: bool = False, + ) -> BinaryEncodingSynthesizer: + """Create a synthesiser, run both stages, and return the solved instance. + + This is the primary entry point. It validates the input RREF matrix, + executes the full two-stage synthesis, and returns the ready-to-export + synthesiser. + + Args: + matrix: Binary (0/1) matrix in RREF or upper-staircase diagonal + form, shaped ``(num_qubits, num_determinants)``. + include_negative_controls: If True, include both positive and + negative (0-valued) fixed controls in PUI blocks. If False, + only positive (1-valued) controls are emitted. + measurement_based_uncompute: If True, emit ``select_and`` ops + that use measurement-based AND uncomputation (requires + Adaptive_RI target profile or higher). + + Returns: + A solved :class:`BinaryEncodingSynthesizer`. + + Raises: + NotRrefError: If *matrix* is not in valid RREF form. + + """ + synth = cls( + RrefTableau(matrix), + include_negative_controls=include_negative_controls, + measurement_based_uncompute=measurement_based_uncompute, + ) + synth.run() + return synth + + def max_batch_size(self) -> int: + """Return the maximum batch size supported by the current tableau shape. + + The batch size is the largest power of 2 that fits within the number + of sparse rows (``num_rows - dense_size``). + + Each batch element occupies a dedicated sparse + row as a one-hot indicator (element *i* has a 1 at sparse row *i*). + Therefore the batch cannot exceed the number of available sparse rows. + """ + sparse_size = self.tableau.num_rows - self.dense_size + assert sparse_size > 0 + if sparse_size & (sparse_size - 1) == 0: + return sparse_size + return 1 << (sparse_size.bit_length() - 1) + + def _record(self, op: tuple[MatrixCompressionType, tuple[Any, ...]]): + """Append an operation and update the tableau.""" + self.circuit.append(op) + kind, payload = op + + if kind is MatrixCompressionType.CX: + self.tableau.cx(*payload) + elif kind is MatrixCompressionType.SWAP: + self.tableau.swap(*payload) + elif kind is MatrixCompressionType.TOFFOLI: + tgt, ctrl_pos, ctrl_row, ctrl_val = payload + self.tableau.toffoli(tgt, (ctrl_pos, True), (ctrl_row, ctrl_val)) + elif kind is MatrixCompressionType.PUI_BLOCK: + fixed_controls, rest_entries = payload + self.tableau.toffoli_pui_fixed(fixed_controls) + for off, ctrls in rest_entries: + self.tableau.toffoli_pui_rest(off, ctrls) + elif kind is MatrixCompressionType.X: + self.tableau.x(*payload) + + def run(self): + """Execute full synthesis and restore original column order.""" + rank, col_perm = self._permute_columns_pivots_first() + + self._run_stage1_diagonal_encoding(rank) + + if self.tableau.num_cols - rank > 0: + stage_two_start = self._choose_stage_two_start_index(rank) + self._run_stage2_non_pivot_col_processing(stage_two_start) + + self._complete_bijection() + self._validate() + + # Remap bijection and tableau back to original column order + self.bijection = [(dv, col_perm[c]) for dv, c in self.bijection] + inv_perm = [0] * len(col_perm) + for new_idx, old_idx in enumerate(col_perm): + inv_perm[old_idx] = new_idx + self.tableau.permute_columns(inv_perm) + + def _validate(self): + """Assert final solver invariants.""" + for row in range(self.dense_size, self.tableau.num_rows): + assert self.tableau.row_is_zero(row), f"Row {row} not zeroed" + assert len(self.bijection) == self.tableau.num_cols, "Bijection incomplete" + + def _permute_columns_pivots_first(self) -> tuple[int, list[int]]: + """Move pivot columns to the front to form a diagonal block. + + Returns: + Tuple ``(rank, col_perm)`` where ``rank`` is the number of pivot + columns and ``col_perm`` is the applied forward permutation. + + """ + pivot_cols = [p[1] for p in self.tableau.pivots] + rank = len(pivot_cols) + pivot_set = set(pivot_cols) + non_pivot_cols = [c for c in range(self.tableau.num_cols) if c not in pivot_set] + + col_perm = pivot_cols + non_pivot_cols + self.tableau.permute_columns(col_perm) + return rank, col_perm + + # --- Stage 1: Diagonal Encoding --- + + def _run_stage1_diagonal_encoding(self, rank: int): + """Stage 1: Diagonal encoding. + + Processes only the rank*rank identity pivot block, assigning contiguous + integer labels (0, 1, 2, …) to the leading pivot columns. + + Two steps: + 1. Unary encoding: CX ladder + X + SWAP converts the identity block + into an upper-staircase (unary) matrix where column ``c`` has 1s in + rows 0 through c-1. + 2. Binary compression: A divide-and-conquer loop folds the unary rows + into binary-counter dense rows using one Toffoli per erased unary + bit, with no ancilla waste. + """ + if rank == 0: + return + + logical_rows = self._apply_unary_staircase(rank) + self._convert_unary_to_binary(rank, logical_rows) + + # Record bijection for the contiguous pivot columns + dense_size = self.dense_size + for c in range(rank): + dense_val = _bits_to_int(self.tableau.data[:dense_size, c]) + self.bijection.append((dense_val, c)) + + def _apply_unary_staircase(self, rank: int) -> list[int]: + """Convert the identity block into an upper-staircase matrix.""" + logical_rows = list(range(rank)) + if not self.tableau.is_diagonal_reduced: + for i in range(rank - 2, -1, -1): + self._record((MatrixCompressionType.CX, (logical_rows[i + 1], logical_rows[i]))) + self._record((MatrixCompressionType.X, (logical_rows[0],))) + return logical_rows + + def _convert_unary_to_binary(self, limit: int, logical_rows: list[int]): + """Fold unary rows into binary-counter dense rows.""" + logical_rows = [*logical_rows[1:], logical_rows[0]] + + if limit > 1: + active_unary = logical_rows[: limit - 1] + leftover_zero = logical_rows[limit - 1] + dense_rows, zero_rows = [], [] + + while len(active_unary) > 1: + accumulator = active_unary[0] + dense_rows.append(accumulator) + unary_bits = active_unary[1:] + next_active_unary = [] + + for p in range(len(unary_bits) // 2): + x, y = unary_bits[2 * p], unary_bits[2 * p + 1] + self._record((MatrixCompressionType.CX, (x, accumulator))) + self._record((MatrixCompressionType.CX, (y, accumulator))) + self._record((MatrixCompressionType.TOFFOLI, (y, accumulator, x, True))) + next_active_unary.append(x) + zero_rows.append(y) + + if len(unary_bits) % 2 == 1: + x = unary_bits[-1] + self._record((MatrixCompressionType.CX, (x, accumulator))) + next_active_unary.append(x) + + active_unary = next_active_unary + + dense_rows.append(active_unary[0]) + dense_rows = dense_rows[::-1] # Reverse to MSB-first + + all_zero_rows = [*zero_rows, leftover_zero] + num_msb_padding = min(self.dense_size - len(dense_rows), len(all_zero_rows)) + final_physical_rows = all_zero_rows[:num_msb_padding] + dense_rows + all_zero_rows[num_msb_padding:] + + # Cycle sort to align physical permutations + current_pos = {i: i for i in range(limit)} + row_at = {i: i for i in range(limit)} + for i in range(limit): + target_row = final_physical_rows[i] + if row_at[i] != target_row: + curr_idx = current_pos[target_row] + self._record((MatrixCompressionType.SWAP, (i, curr_idx))) + swapped_row = row_at[i] + row_at[curr_idx], current_pos[swapped_row] = swapped_row, curr_idx + row_at[i], current_pos[target_row] = target_row, i + + # --- Stage 2: Non-Pivot Processing --- + + def _choose_stage_two_start_index(self, rank: int) -> int: + """Choose Stage Two start label to reduce first-batch PUI cost. + + Prefer starting at the next ``max_batch_size`` boundary so the first + Stage Two batch is already alignment-friendly. This is only safe when + enough dense-label capacity remains to encode all non-pivot columns. + + If capacity is insufficient, return ``rank`` and allow Stage Two to + flush an early partial batch to reach the next aligned boundary. + """ + mbs = self.max_batch_size() + if mbs <= 1: + return rank + + next_aligned = ((rank + mbs - 1) // mbs) * mbs + if next_aligned == rank: + return rank + + non_pivot_cols = self.tableau.num_cols - rank + return next_aligned if (next_aligned + non_pivot_cols) <= (1 << self.dense_size) else rank + + def _run_stage2_non_pivot_col_processing(self, k_start: int): + """Stage 2: Non-pivot column processing. + + For each unmapped non-pivot column, locates the next actionable element, + synthesises the target dense row pattern via CX adjustments, and + normalises the sparse indicator bit into a one-hot batch row. + + Batches are flushed mid-loop (emitting a PUI block) whenever they reach + ``max_batch_size`` or would cross an alignment boundary, because sparse + indicator rows are reused across batches. The final (partial) batch is + flushed at the end of the loop, and any remaining edge case is resolved. + """ + mbs = self.max_batch_size() + self.batch_index = k_start + mapped_cols: set[int] = {col for _, col in self.bijection} + + while True: + if self.batch: + new_len = len(self.batch) + 1 + block_shift = _dense_qubits_size(new_len) + crosses = (self.batch[0].dense_content >> block_shift) != (self.batch_index >> block_shift) + + if new_len > mbs or crosses: + self._clear_sparse_bits() + self.batch.clear() + + target_row = self.dense_size + len(self.batch) + element = self._find_next_non_zero_element(target_row, mapped_cols) + + if element is not None: + target_col = self._create_target_row(target_row, element) + self._permute_col_and_add_to_batch(target_col, target_row) + mapped_cols.add(target_col) + else: + if self.batch: + self._clear_sparse_bits() + self.batch.clear() + continue + break + + def _find_next_non_zero_element(self, target_row: int, mapped_cols: set[int]) -> tuple[bool, int, int] | None: + """Find next actionable non-zero element using fast numpy slicing. + + Args: + target_row: First sparse row to scan for direct one-hot markers. + mapped_cols: Column indices already assigned in the bijection; + passed by the caller to avoid repeated reconstruction. + + Returns: + Triple ``(is_direct, col, row)`` for the best candidate, or + ``None`` when every unmapped column is fully zeroed in the + accessible rows. + + """ + unmapped_cols = [c for c in range(self.tableau.num_cols) if c not in mapped_cols] + if not unmapped_cols: + return None + + # 1. Check direct sparse rows + sub_data = self.tableau.data[target_row:, unmapped_cols] + rows, cols = np.nonzero(sub_data) + if rows.size > 0: + return (True, unmapped_cols[cols[0]], target_row + rows[0]) + + # 2. Check current batch indicators + for i, be in enumerate(self.batch): + brow = self.dense_size + i + for col in unmapped_cols: + if be.col is not None and col == be.col: + continue + if self.tableau.data[brow, col]: + return (False, col, brow) + return None + + def _create_target_row(self, target_row: int, element: tuple[bool, int, int]) -> int: + """Create/normalize the next target-row element and return its column. + + Args: + target_row: Sparse row that will host the one-hot batch marker. + element: Triple ``(is_direct, col, row)`` from + :meth:`_find_next_non_zero_element`. + + Returns: + Column index selected for insertion into the current batch. + + """ + is_direct, col, row = element + if is_direct: + if row != target_row: + self._record((MatrixCompressionType.SWAP, (target_row, row))) + return col + + self._synthesize_target_row(target_row, col, row) + return col + + def _synthesize_target_row(self, target_row: int, col: int, row: int): + """Synthesize a target row element utilizing vectorized array masking. + + When the only non-zero entry for an unmapped column lives in an + already-batched row, a Toffoli is emitted to create a fresh indicator + at ``target_row`` by exploiting a difference between the expected and + actual column contents. + + Args: + target_row: Destination sparse row for the new indicator. + col: Unmapped column to process. + row: Existing batch row where the non-zero entry was found. + + """ + self.bad_element_count += 1 + batch_idx = row - self.dense_size + batch_element_bits = _int_to_bits(self.batch[batch_idx].dense_content, self.dense_size) + + is_batch_index = [i == batch_idx for i in range(self.tableau.num_rows - self.dense_size)] + combined_idx = np.array(batch_element_bits + is_batch_index, dtype=bool) + + col_data = self.tableau.get_col(col).astype(bool) + + # Find differing row, ignoring the current 'row' + diffs = combined_idx != col_data + diffs[row] = False + + diff_row = int(np.flatnonzero(diffs)[0]) + diff_val = bool(col_data[diff_row]) + + self._record((MatrixCompressionType.TOFFOLI, (target_row, row, diff_row, diff_val))) + + def _permute_col_and_add_to_batch(self, current_col: int, ctrl_row: int): + """Normalize a column's dense/sparse bits and append it to the batch. + + Emits CX gates controlled by ``ctrl_row`` to align the dense register + to ``batch_index`` and the sparse register to a one-hot marker, then + records the new :class:`_BatchElement` and bijection entry. + + Args: + current_col: Column being processed. + ctrl_row: Sparse row whose 1-entry controls the CX corrections. + + """ + dense_size = self.dense_size + k_bits = np.array(_int_to_bits(self.batch_index, dense_size), dtype=bool) + + # Align dense qubits + dense_col_data = self.tableau.data[:dense_size, current_col].astype(bool) + for d_qubit in np.flatnonzero(dense_col_data != k_bits): + self._record((MatrixCompressionType.CX, (ctrl_row, int(d_qubit)))) + + # Align sparse qubits to isolate the one-hot marker + sparse_col_data = self.tableau.data[dense_size:, current_col].astype(bool) + target_bits = np.zeros(self.tableau.num_rows - dense_size, dtype=bool) + target_bits[len(self.batch)] = True + + for s_qubit in np.flatnonzero(sparse_col_data != target_bits): + self._record((MatrixCompressionType.CX, (ctrl_row, dense_size + int(s_qubit)))) + + self.batch.append(_BatchElement(current_col, self.batch_index)) + self.bijection.append((self.batch_index, current_col)) + self.batch_index += 1 + + def _complete_bijection(self): + """Fill missing bijection entries from current dense column contents.""" + mapped = {col for _, col in self.bijection} + self.bijection.extend( + (_bits_to_int(self.tableau.data[: self.dense_size, c]), c) + for c in range(self.tableau.num_cols) + if c not in mapped + ) + + # --- PUI Lowering & Exporting --- + + def _clear_sparse_bits(self): + """Emit a PUI block that zeroes all sparse indicator rows for the current batch.""" + assert self.batch + dense_size = self.dense_size + num_changing = _dense_qubits_size(len(self.batch)) + num_fixed = dense_size - num_changing + k0 = self.batch[0].dense_content + + fixed_controls = [ + (r, bool((k0 >> (dense_size - 1 - r)) & 1)) + for r in range(num_fixed) + if self.include_negative_controls or ((k0 >> (dense_size - 1 - r)) & 1) + ] + + rest_entries = [] + for i, be in enumerate(self.batch): + changing_controls = [ + ( + num_fixed + off, + bool((be.dense_content >> (dense_size - 1 - num_fixed - off)) & 1), + ) + for off in range(num_changing) + ] + rest_entries.append((i, changing_controls)) + + self._record((MatrixCompressionType.PUI_BLOCK, (fixed_controls, rest_entries))) + + def to_gf2x_operations( + self, + num_local_qubits: int, + active_qubit_indices: list[int] | None = None, + ancilla_start: int | None = None, + ) -> tuple[list[tuple[str, Any]], int]: + """Translate recorded circuit operations into GF2+X instruction tuples. + + Args: + num_local_qubits: Index where ancilla allocation can start. + active_qubit_indices: Optional mapping from local qubit index (0..num_local_qubits-1) + to global qubit index. If provided, operations are translated to global indices. + ancilla_start: Optional global starting index for ancillas. Used if + active_qubit_indices is provided. + + Returns: + Pair ``(ops, num_ancilla)`` with generated operations (optionally translated) + and the total ancilla count consumed by lookup blocks. + + """ + ops: list[tuple[str, Any]] = [] + start_ancilla_idx = num_local_qubits + max_ancilla_idx = start_ancilla_idx - 1 + + for kind, payload in self.circuit: + if kind is MatrixCompressionType.CX: + ops.append(("cx", payload)) + elif kind is MatrixCompressionType.SWAP: + ops.append(("swap", payload)) + elif kind is MatrixCompressionType.TOFFOLI: + target, ctrl_pos, ctrl_row, ctrl_val = payload + if not ctrl_val: + ops.append(("x", ctrl_row)) + ops.append(("ccx", (target, ctrl_pos, ctrl_row))) + if not ctrl_val: + ops.append(("x", ctrl_row)) + elif kind is MatrixCompressionType.X: + ops.append(("x", payload[0])) + elif kind is MatrixCompressionType.PUI_BLOCK: + fixed_controls, rest_entries = payload + new_max = self._flush_pui_lookup_block( + ops, + self.dense_size, + fixed_controls, + rest_entries, + start_ancilla_idx, + ) + max_ancilla_idx = max(max_ancilla_idx, new_max) + + num_ancilla = max(0, max_ancilla_idx - start_ancilla_idx + 1) + + if active_qubit_indices is not None and ancilla_start is not None: + ops = self._translate_ops(ops, num_local_qubits, active_qubit_indices, ancilla_start) + + return ops, num_ancilla + + @staticmethod + def _translate_ops( + ops: list[tuple[str, Any]], + num_local_qubits: int, + active_qubit_indices: list[int], + ancilla_start: int, + ) -> list[tuple[str, Any]]: + """Remap local qubit indices to global topological indices. + + Indices below ``num_local_qubits`` are mapped through + ``active_qubit_indices``; higher indices are treated as ancillae + starting at ``ancilla_start``. + + Args: + ops: Operation list with local indices. + num_local_qubits: Boundary between active and ancilla indices. + active_qubit_indices: Local-to-global mapping for active qubits. + ancilla_start: Global start index for ancilla qubits. + + Returns: + New operation list with all qubit indices remapped. + + """ + + def map_idx(idx: int) -> int: + return ( + int(active_qubit_indices[idx]) if idx < num_local_qubits else ancilla_start + (idx - num_local_qubits) + ) + + translated: list[tuple[str, Any]] = [] + for op_name, op_args in ops: + if op_name == "x": + translated.append(("x", map_idx(int(op_args)))) + elif op_name in {"cx", "swap"}: + translated.append((op_name, (map_idx(int(op_args[0])), map_idx(int(op_args[1]))))) + elif op_name in {"ccx"}: + translated.append((op_name, tuple(map_idx(int(a)) for a in op_args))) + elif op_name == "mcx": + controls, ctrl_state, target = op_args + translated.append( + ( + "mcx", + ( + [map_idx(int(q)) for q in controls], + list(ctrl_state), + map_idx(int(target)), + ), + ) + ) + elif op_name in ("select", "select_and"): + data_table, addr_qubits, dat_qubits = op_args + translated.append( + ( + op_name, + ( + data_table, + [map_idx(int(q)) for q in addr_qubits], + [map_idx(int(q)) for q in dat_qubits], + ), + ) + ) + else: + translated.append((op_name, op_args)) + return translated + + def _flush_pui_lookup_block( + self, + ops: list[tuple[str, Any]], + sbs: int, + fixed_controls: list[tuple[int, bool]], + rest_entries: list[tuple[int, list[tuple[int, bool]]]], + start_ancilla_idx: int, + ) -> int: + """Convert one recorded PUI block into lookup-based GF2+X operations. + + Args: + ops: Destination operation list to append into. + sbs: Dense-register width. + fixed_controls: Shared controls for all block entries. + rest_entries: Per-target offsets and changing controls. + start_ancilla_idx: First ancilla index available for lookup synth. + + Returns: + Maximum ancilla index used by this block, or + ``start_ancilla_idx - 1`` when no ancilla is required. + + """ + if not rest_entries: + return start_ancilla_idx - 1 + + mono_ops, mono_max, mono_and = self._synthesize_single_pui_lookup_block( + sbs, + fixed_controls, + rest_entries, + start_ancilla_idx, + ) + + chunked = self._split_rest_entries_into_power_of_two_chunks(rest_entries) + if len(chunked) <= 1: + ops.extend(mono_ops) + return mono_max + + chunked_ops, chunked_max, chunked_and = [], start_ancilla_idx - 1, 0 + for chunk in chunked: + sub_ops, sub_max, sub_and = self._synthesize_single_pui_lookup_block( + sbs, + fixed_controls, + chunk, + start_ancilla_idx, + ) + chunked_ops.extend(sub_ops) + chunked_max = max(chunked_max, sub_max) + chunked_and += sub_and + + if chunked_and <= mono_and: + ops.extend(chunked_ops) + return chunked_max + + ops.extend(mono_ops) + return mono_max + + def _synthesize_single_pui_lookup_block( + self, + sbs: int, + fixed_controls: list[tuple[int, bool]], + rest_entries: list[tuple[int, list[tuple[int, bool]]]], + start_ancilla_idx: int, + ) -> tuple[list[tuple[str, Any]], int, int]: + """Lower one PUI sub-block into lookup ops. + + Returns: + ``(ops, max_ancilla_idx, and_count)`` where ``and_count`` is the + number of emitted ``and`` operations, used as a Toffoli-cost proxy. + + """ + if not rest_entries: + return [], start_ancilla_idx - 1, 0 + + fixed_controls, rest_entries = self._canonicalize_pui_controls(fixed_controls, rest_entries) + address_qubits = self._collect_pui_address_qubits(fixed_controls, rest_entries) + data_qubits = [sbs + offset for offset, _ in rest_entries] + + filtered_table = self._build_pui_lookup_table(fixed_controls, rest_entries, address_qubits) + if not filtered_table: + return [], start_ancilla_idx - 1, 0 + + lookup_ops, num_ancilla_used = _lookup_select( + filtered_table, + address_qubits=address_qubits, + data_qubits=data_qubits, + use_measurement_and=self.measurement_based_uncompute, + ) + + typed_lookup_ops = cast("list[tuple[str, Any]]", lookup_ops) + gf2x_ops = list(reversed(typed_lookup_ops)) + and_count = sum(1 for name, _ in typed_lookup_ops if name == "and") + + max_ancilla = start_ancilla_idx + num_ancilla_used - 1 if num_ancilla_used > 0 else start_ancilla_idx - 1 + return gf2x_ops, max_ancilla, and_count + + def _canonicalize_pui_controls( + self, + fixed_controls: list[tuple[int, bool]], + rest_entries: list[tuple[int, list[tuple[int, bool]]]], + ) -> tuple[list[tuple[int, bool]], list[tuple[int, list[tuple[int, bool]]]]]: + """Promote chunk-local constant controls from changing to fixed. + + For a given block, rows that appear in every entry with the same value + do not need to remain in per-entry changing controls. + """ + if not rest_entries: + return fixed_controls, rest_entries + + n_entries = len(rest_entries) + fixed_map = dict(fixed_controls) + + # Single pass: count occurrences and collect unique values per row + row_info: dict[int, tuple[int, set[bool]]] = {} + for _, changing_controls in rest_entries: + for row, val in changing_controls: + count, vals = row_info.get(row, (0, set())) + vals.add(bool(val)) + row_info[row] = (count + 1, vals) + + # Promote rows that appear in all entries with a single value + for row, (count, values) in row_info.items(): + if count == n_entries and len(values) == 1: + promoted_val = next(iter(values)) + if row not in fixed_map or fixed_map[row] == promoted_val: + fixed_map[row] = promoted_val + + fixed_rows = set(fixed_map) + simplified_rest = [(off, [(r, v) for r, v in ctrls if r not in fixed_rows]) for off, ctrls in rest_entries] + return sorted(fixed_map.items()), simplified_rest + + def _split_rest_entries_into_power_of_two_chunks( + self, rest_entries: list[tuple[int, list[tuple[int, bool]]]] + ) -> list[list[tuple[int, list[tuple[int, bool]]]]]: + """Split entries into contiguous power-of-two chunks. + + This keeps control patterns local while converting expensive + non-power-of-two lookup tables into cheaper composable pieces. + """ + n = len(rest_entries) + if n <= 2: + return [rest_entries] + + chunks, i, remaining = [], 0, n + while remaining > 0: + chunk_size = 1 << (remaining.bit_length() - 1) + chunks.append(rest_entries[i : i + chunk_size]) + i += chunk_size + remaining -= chunk_size + return chunks + + def _collect_pui_address_qubits( + self, + fixed_controls: list[tuple[int, bool]], + rest_entries: list[tuple[int, list[tuple[int, bool]]]], + ) -> list[int]: + """Collect and sort all control rows that address a PUI lookup table.""" + all_ctrl_rows = {row for row, _ in fixed_controls} + for _, changing_controls in rest_entries: + all_ctrl_rows.update(row for row, _ in changing_controls) + return sorted(all_ctrl_rows) + + def _build_pui_lookup_table( + self, + fixed_controls: list[tuple[int, bool]], + rest_entries: list[tuple[int, list[tuple[int, bool]]]], + address_qubits: list[int], + ) -> dict[tuple[int, ...], tuple[int, ...]]: + """Build sparse truth table for one PUI lookup block. + + Returns: + Mapping from address bit tuples to one-hot output tuples, with + all-zero outputs omitted. + + """ + n_outputs = len(rest_entries) + table: dict[tuple[int, ...], tuple[int, ...]] = {} + for i, (_, changing_controls) in enumerate(rest_entries): + ctrl_map = {**dict(fixed_controls), **dict(changing_controls)} + address = tuple(int(ctrl_map[row]) for row in address_qubits) + data = tuple(1 if j == i else 0 for j in range(n_outputs)) + table[address] = data + + return table + + +def _lookup_select( + table_dict: dict[tuple[int, ...], tuple[int, ...]], + address_qubits: list[int], + data_qubits: list[int], + *, + use_measurement_and: bool = False, +) -> tuple[list[tuple[str, Any]], int]: + """Synthesize a lookup-based select or select_and operation for a given truth table. + + Args: + table_dict: Mapping from address bit tuples to output bit tuples, with all-zero outputs omitted. + address_qubits: Qubit indices corresponding to the address bits. + data_qubits: Qubit indices corresponding to the data bits. + use_measurement_and: If True, emit ``select_and`` ops that use measurement-based AND uncomputation + (requires Adaptive_RI target profile or higher). + If False, emit standard ``select`` ops with internal ancilla management. + The choice affects the number of ancillas used and the structure of the emitted operations. + + Returns: + Tuple ``(ops, num_ancilla_used)`` where ``ops`` is a list of GF2+X operations implementing the lookup, + and ``num_ancilla_used`` is the number of ancilla qubits consumed. + + """ + if not table_dict: + return [], 0 + + operations: list[tuple[str, Any]] = [] + + n_address = len(address_qubits) + n_data = len(data_qubits) + n_entries = 1 << n_address + + # Build dense Bool[][] table from sparse dict. + # addr_tuple uses little-endian bit ordering: addr_tuple[0] = LSB. + data_table: list[list[bool]] = [[False] * n_data for _ in range(n_entries)] + for addr_tuple, data_tuple in table_dict.items(): + addr_int = sum(int(bit) << i for i, bit in enumerate(addr_tuple)) + data_table[addr_int] = [bool(b) for b in data_tuple] + + # Our table index also uses little-endian (addr_tuple[0] = LSB), so + # pass address_qubits directly without reversing. + op_name = "select_and" if use_measurement_and else "select" + operations.append((op_name, (data_table, list(address_qubits), list(data_qubits)))) + + # Select uses no ancilla qubits (managed internally by Q#). + num_ancilla_used = 0 + return operations, num_ancilla_used diff --git a/python/src/qdk_chemistry/utils/qsharp/__init__.py b/python/src/qdk_chemistry/utils/qsharp/__init__.py index 3cbe37545..a2ddf7301 100644 --- a/python/src/qdk_chemistry/utils/qsharp/__init__.py +++ b/python/src/qdk_chemistry/utils/qsharp/__init__.py @@ -7,40 +7,25 @@ from pathlib import Path import qdk -from qdk import qsharp - -__all__ = ["QSHARP_UTILS"] - -_QS_FILES = [ - Path(__file__).parent / "StatePreparation.qs", - Path(__file__).parent / "IterativePhaseEstimation.qs", - Path(__file__).parent / "ControlledPauliExp.qs", - Path(__file__).parent / "MeasurementBasis.qs", -] - - -def get_qsharp_utils(): - """Returns the Q# namespace for chemistry operations (lazy-loaded).""" - try: - return qdk.code.QDKChemistry.Utils - except AttributeError: - code = "\n".join(f.read_text() for f in _QS_FILES) - qsharp.eval(code) - return qdk.code.QDKChemistry.Utils +import qsharp +from qdk import init as qdk_init +from qsharp._qsharp import get_config as get_qdk_profile_config +from qdk_chemistry.utils import Logger -class _QSharpUtilsProxy: - """Lightweight proxy that lazily resolves the Q# utilities namespace.""" - - def __getattr__(self, name: str): - """Load Q# code (if necessary) and resolve *name* on the utilities namespace. - - Args: - name: The name of the attribute being accessed on the Q# utilities namespace. - - """ - utils = get_qsharp_utils() - return getattr(utils, name) - +__all__ = ["QSHARP_UTILS"] -QSHARP_UTILS = _QSharpUtilsProxy() +# Initialize Q# interpreter +qdk_config = get_qdk_profile_config() +_QDK_INTERPRETER_PROFILE = qdk_config.get_target_profile() +if _QDK_INTERPRETER_PROFILE == "unrestricted": # Default by Q# if not set + _QDK_INTERPRETER_PROFILE = qsharp.TargetProfile.Adaptive_RIF + Logger.debug( + f"QDK interpreter profile initialized to '{_QDK_INTERPRETER_PROFILE}'. " + "If you imported Q# code before this module was loaded, please re-import it, " + "or set your target profile before importing qdk_chemistry." + ) + +_QS_DIR = Path(__file__).parent +qdk_init(project_root=_QS_DIR, target_profile=_QDK_INTERPRETER_PROFILE) +QSHARP_UTILS = qdk.code.QDKChemistry.Utils diff --git a/python/src/qdk_chemistry/utils/qsharp/qsharp.json b/python/src/qdk_chemistry/utils/qsharp/qsharp.json new file mode 100644 index 000000000..ffbc3c383 --- /dev/null +++ b/python/src/qdk_chemistry/utils/qsharp/qsharp.json @@ -0,0 +1,11 @@ +{ + "author": "Microsoft", + "license": "MIT", + "files": [ + "src/BinaryEncoding.qs", + "src/StatePreparation.qs", + "src/IterativePhaseEstimation.qs", + "src/ControlledPauliExp.qs", + "src/MeasurementBasis.qs" + ] +} diff --git a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs new file mode 100644 index 000000000..e6eb2b126 --- /dev/null +++ b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for +// license information. + +namespace QDKChemistry.Utils.BinaryEncoding { + + import Std.Arrays.MostAndTail; + import Std.Arrays.Partitioned; + import Std.Arrays.Subarray; + import Std.Canon.ApplyControlledOnBitString; + import Std.Convert.IntAsDouble; + import Std.Math.Ceiling; + import Std.Math.Lg; + import Std.Measurement.MResetX; + import Std.StatePreparation.PreparePureStateD; + + /// A single gate produced by the matrix compression pipeline. + /// + /// ``qubits`` always contains qubit indices only: + /// ("X", [target], 0, []) + /// ("CX", [control, target], 0, []) + /// ("SWAP", [a, b], 0, []) + /// ("CCX", [ctrl1, ctrl2, target], 0, []) + /// ("MCX", [target, ctrl0, ctrl1, ...], ctrlStateBitmask, []) + /// ("SELECT", [addr0..addrN, data0..dataM], numAddrQubits, data[][]) + struct MatrixCompressionOp { + name : String, + qubits : Int[], + controlState : Int, + lookupData : Bool[][], + } + + /// Parameters for the binary-encoding state preparation. + struct BinaryEncodingStatePreparationParams { + /// Qubit indices for the dense state preparation (row map, reversed). + rowMap : Int[], + /// Amplitudes of the reduced-space statevector. + stateVector : Double[], + /// GF2+X expansion operations (CX / X) to reverse the GF2+X elimination. + expansionOps : MatrixCompressionOp[], + /// Binary-encoding gate sequence (already reversed by Python). + binaryEncodingOps : MatrixCompressionOp[], + /// Total number of qubits (system + ancilla). + numQubits : Int, + /// Number of ancilla qubits required by the binary-encoding circuit. + numAncilla : Int, + } + + /// Apply a single matrix-compression gate to a qubit register. + operation ApplyMatrixCompressionOp(gate : MatrixCompressionOp, qs : Qubit[]) : Unit { + if gate.name == "X" { + X(qs[gate.qubits[0]]); + } elif gate.name == "CX" { + CX(qs[gate.qubits[0]], qs[gate.qubits[1]]); + } elif gate.name == "SWAP" { + SWAP(qs[gate.qubits[0]], qs[gate.qubits[1]]); + } elif gate.name == "CCX" { + CCNOT(qs[gate.qubits[0]], qs[gate.qubits[1]], qs[gate.qubits[2]]); + } elif gate.name == "MCX" { + let target = gate.qubits[0]; + let numControls = Length(gate.qubits) - 1; + mutable controlQubits = []; + mutable ctrlStateBools = []; + for i in 0..numControls - 1 { + set controlQubits += [qs[gate.qubits[1 + i]]]; + set ctrlStateBools += [((gate.controlState >>> i) &&& 1) == 1]; + } + ApplyControlledOnBitString(ctrlStateBools, X, controlQubits, qs[target]); + } elif gate.name == "SELECT" { + let numAddr = gate.controlState; + mutable addrQubits : Qubit[] = []; + mutable targetQubits : Qubit[] = []; + for i in 0..Length(gate.qubits) - 1 { + if i < numAddr { + set addrQubits += [qs[gate.qubits[i]]]; + } else { + set targetQubits += [qs[gate.qubits[i]]]; + } + } + SparseOneHotSelect(gate.lookupData, addrQubits, targetQubits, false); + } elif gate.name == "SELECT_AND" { + let numAddr = gate.controlState; + mutable addrQubits : Qubit[] = []; + mutable targetQubits : Qubit[] = []; + for i in 0..Length(gate.qubits) - 1 { + if i < numAddr { + set addrQubits += [qs[gate.qubits[i]]]; + } else { + set targetQubits += [qs[gate.qubits[i]]]; + } + } + SparseOneHotSelect(gate.lookupData, addrQubits, targetQubits, true); + } else { + fail $"Unknown gate name: {gate.name}"; + } + } + + /// Return true when every row of ``data`` is all-false. + function IsDataAllZeros(data : Bool[][]) : Bool { + for row in data { + for bit in row { + if bit { return false; } + } + } + return true; + } + + /// Apply X to each target qubit where the corresponding data bit is true. + operation WriteOneHotData(data : Bool[], target : Qubit[]) : Unit { + for i in 0..Length(data) - 1 { + if data[i] { X(target[i]); } + } + } + + /// Controlled variant: apply CX(ctl, target[i]) for each true bit. + operation ControlledWriteOneHotData(ctl : Qubit, data : Bool[], target : Qubit[]) : Unit { + for i in 0..Length(data) - 1 { + if data[i] { CX(ctl, target[i]); } + } + } + + + /// AND gate with measurement-based adjoint uncomputation. + operation MeasurementBasedAND(a : Qubit, b : Qubit, target : Qubit) : Unit is Adj { + body (...) { + CCNOT(a, b, target); + } + adjoint (...) { + if MResetX(target) == One { + CZ(a, b); + } + } + } + + /// Sparse one-hot select + /// + /// For each row of the data, apply X to the target bits where the row is true, controlled on the address qubits being in the state corresponding to that row. + operation SparseOneHotSelect( + data : Bool[][], + address : Qubit[], + target : Qubit[], + useMeasurementAND : Bool + ) : Unit { + let N = Length(data); + + if N == 0 or IsDataAllZeros(data) { + // Nothing to apply + } elif N == 1 { + WriteOneHotData(data[0], target); + } else { + let n = Ceiling(Lg(IntAsDouble(N))); + let (most, tail) = MostAndTail(address[...n - 1]); + let parts = Partitioned([2^(n - 1)], data); + let leftEmpty = IsDataAllZeros(parts[0]); + let rightEmpty = IsDataAllZeros(parts[1]); + + if not leftEmpty and not rightEmpty { + within { X(tail); } apply { + SparseOneHotSCS(tail, parts[0], most, target, useMeasurementAND); + } + SparseOneHotSCS(tail, parts[1], most, target, useMeasurementAND); + } elif not rightEmpty { + SparseOneHotSCS(tail, parts[1], most, target, useMeasurementAND); + } elif not leftEmpty { + within { X(tail); } apply { + SparseOneHotSCS(tail, parts[0], most, target, useMeasurementAND); + } + } + } + } + + /// Singly-controlled recursion for SparseOneHotSelect. + /// + /// When ``useMeasurementAND`` is true, uses MeasurementBasedAND + Adjoint + operation SparseOneHotSCS( + ctl : Qubit, + data : Bool[][], + address : Qubit[], + target : Qubit[], + useMeasurementAND : Bool + ) : Unit { + let N = Length(data); + + if N == 0 or IsDataAllZeros(data) { + // Skip empty branch + } elif N == 1 { + ControlledWriteOneHotData(ctl, data[0], target); + } else { + let n = Ceiling(Lg(IntAsDouble(N))); + let (most, tail) = MostAndTail(address[...n - 1]); + let parts = Partitioned([2^(n - 1)], data); + let leftEmpty = IsDataAllZeros(parts[0]); + let rightEmpty = IsDataAllZeros(parts[1]); + + if not leftEmpty and not rightEmpty { + use helper = Qubit(); + if useMeasurementAND { + within { X(tail); } apply { + MeasurementBasedAND(ctl, tail, helper); + } + SparseOneHotSCS(helper, parts[0], most, target, true); + CNOT(ctl, helper); + SparseOneHotSCS(helper, parts[1], most, target, true); + Adjoint MeasurementBasedAND(ctl, tail, helper); + } else { + within { X(tail); } apply { + CCNOT(ctl, tail, helper); + } + SparseOneHotSCS(helper, parts[0], most, target, false); + CNOT(ctl, helper); + SparseOneHotSCS(helper, parts[1], most, target, false); + CCNOT(ctl, tail, helper); + } + } elif not rightEmpty { + use helper = Qubit(); + if useMeasurementAND { + MeasurementBasedAND(ctl, tail, helper); + SparseOneHotSCS(helper, parts[1], most, target, true); + Adjoint MeasurementBasedAND(ctl, tail, helper); + } else { + CCNOT(ctl, tail, helper); + SparseOneHotSCS(helper, parts[1], most, target, false); + CCNOT(ctl, tail, helper); + } + } elif not leftEmpty { + use helper = Qubit(); + if useMeasurementAND { + X(tail); + MeasurementBasedAND(ctl, tail, helper); + SparseOneHotSCS(helper, parts[0], most, target, true); + Adjoint MeasurementBasedAND(ctl, tail, helper); + X(tail); + } else { + X(tail); + CCNOT(ctl, tail, helper); + SparseOneHotSCS(helper, parts[0], most, target, false); + CCNOT(ctl, tail, helper); + X(tail); + } + } + } + } + + /// Prepare a quantum state using GF2+X elimination followed by binary-encoding circuit synthesis. + /// + /// The procedure is: + /// 1. Prepare the dense statevector on the reduced qubit subset. + /// 2. Apply the binary-encoding operations (already reversed by Python). + /// 3. Apply the GF2+X expansion operations (CX / X). + operation BinaryEncodingStatePreparation( + params : BinaryEncodingStatePreparationParams, + qs : Qubit[], + ) : Unit { + // Step 1: Dense state prep on reduced subspace + PreparePureStateD(params.stateVector, Subarray(params.rowMap, qs)); + + // Step 2: Apply binary-encoding ops (pre-reversed by Python) + for gate in params.binaryEncodingOps { + ApplyMatrixCompressionOp(gate, qs); + } + + // Step 3: Expand back via GF2+X operations + for gate in params.expansionOps { + ApplyMatrixCompressionOp(gate, qs); + } + } + + /// Create a callable for the binary-encoding state preparation. + function MakeBinaryEncodingStatePreparationOp( + params : BinaryEncodingStatePreparationParams, + ) : Qubit[] => Unit { + BinaryEncodingStatePreparation(params, _) + } + + /// Top-level circuit entry point for binary-encoding state preparation. + operation MakeBinaryEncodingStatePreparationCircuit( + rowMap : Int[], + stateVector : Double[], + expansionOps : MatrixCompressionOp[], + binaryEncodingOps : MatrixCompressionOp[], + numQubits : Int, + numAncilla : Int, + ) : Unit { + use qs = Qubit[numQubits + numAncilla]; + BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + binaryEncodingOps = binaryEncodingOps, + numQubits = numQubits, + numAncilla = numAncilla, + }, qs); + } + + /// Simulate the binary-encoding state preparation and measure system qubits. + /// + /// This is a verification helper: it runs the full state preparation, + /// measures only the system qubits (not ancilla), resets everything, + /// and returns the measurement results. + import Std.Measurement.MeasureEachZ; + + operation VerifyBinaryEncodingStatePrep( + rowMap : Int[], + stateVector : Double[], + expansionOps : MatrixCompressionOp[], + binaryEncodingOps : MatrixCompressionOp[], + numQubits : Int, + numAncilla : Int, + ) : Result[] { + use qs = Qubit[numQubits + numAncilla]; + BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { + rowMap = rowMap, + stateVector = stateVector, + expansionOps = expansionOps, + binaryEncodingOps = binaryEncodingOps, + numQubits = numQubits, + numAncilla = numAncilla, + }, qs); + let results = MeasureEachZ(qs[0..numQubits - 1]); + ResetAll(qs); + return results + } +} diff --git a/python/src/qdk_chemistry/utils/qsharp/ControlledPauliExp.qs b/python/src/qdk_chemistry/utils/qsharp/src/ControlledPauliExp.qs similarity index 100% rename from python/src/qdk_chemistry/utils/qsharp/ControlledPauliExp.qs rename to python/src/qdk_chemistry/utils/qsharp/src/ControlledPauliExp.qs diff --git a/python/src/qdk_chemistry/utils/qsharp/IterativePhaseEstimation.qs b/python/src/qdk_chemistry/utils/qsharp/src/IterativePhaseEstimation.qs similarity index 100% rename from python/src/qdk_chemistry/utils/qsharp/IterativePhaseEstimation.qs rename to python/src/qdk_chemistry/utils/qsharp/src/IterativePhaseEstimation.qs diff --git a/python/src/qdk_chemistry/utils/qsharp/MeasurementBasis.qs b/python/src/qdk_chemistry/utils/qsharp/src/MeasurementBasis.qs similarity index 100% rename from python/src/qdk_chemistry/utils/qsharp/MeasurementBasis.qs rename to python/src/qdk_chemistry/utils/qsharp/src/MeasurementBasis.qs diff --git a/python/src/qdk_chemistry/utils/qsharp/StatePreparation.qs b/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs similarity index 87% rename from python/src/qdk_chemistry/utils/qsharp/StatePreparation.qs rename to python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs index 5aa739515..d9e3d50d6 100644 --- a/python/src/qdk_chemistry/utils/qsharp/StatePreparation.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs @@ -5,17 +5,23 @@ namespace QDKChemistry.Utils.StatePreparation { import Std.Arrays.Subarray; + import Std.Canon.ApplyControlledOnBitString; + import Std.Measurement.MResetZ; import Std.StatePreparation.PreparePureStateD; + import Std.TableLookup.Select; + import QDKChemistry.Utils.BinaryEncoding.MatrixCompressionOp; + import QDKChemistry.Utils.BinaryEncoding.ApplyMatrixCompressionOp; + /// A struct to hold parameters for state preparation. /// - `rowMap`: An array of integers representing the mapping of qubits to rows in the state vector. /// - `stateVector`: An array of doubles representing the amplitudes of the quantum state. - /// - `expansionOps`: An array of arrays of integers representing the operations to expand the state preparation (e.g., CNOTs, X gates). + /// - `expansionOps`: An array of MatrixCompressionOp representing the operations to expand the state preparation (e.g., CX, X gates). /// - `numQubits`: The number of qubits to allocate for the state preparation. struct StatePreparationParams { rowMap : Int[], stateVector : Double[], - expansionOps : Int[][], + expansionOps : MatrixCompressionOp[], numQubits : Int, } @@ -31,14 +37,8 @@ namespace QDKChemistry.Utils.StatePreparation { qs : Qubit[], ) : Unit { PreparePureStateD(params.stateVector, Subarray(params.rowMap, qs)); - for op in params.expansionOps { - if Length(op) == 2 { - CNOT(qs[op[0]], qs[op[1]]); - } elif Length(op) == 1 { - X(qs[op[0]]); - } else { - fail "Unsupported operation length in expansionOps."; - } + for gate in params.expansionOps { + ApplyMatrixCompressionOp(gate, qs); } } @@ -56,14 +56,14 @@ namespace QDKChemistry.Utils.StatePreparation { /// # Parameters /// - `rowMap`: An array of integers representing the mapping of qubits to rows in the state vector. /// - `stateVector`: An array of doubles representing the amplitudes of the quantum state. - /// - `expansionOps`: An array of arrays of integers representing the operations to expand the state preparation (e.g., CNOTs, X gates). + /// - `expansionOps`: An array of MatrixCompressionOp representing the operations to expand the state preparation. /// - `numQubits`: The number of qubits to allocate for the state preparation. /// # Returns /// - `Unit`: The operation prepares the quantum state on the allocated qubits. operation MakeStatePreparationCircuit( rowMap : Int[], stateVector : Double[], - expansionOps : Int[][], + expansionOps : MatrixCompressionOp[], numQubits : Int, ) : Unit { use qs = Qubit[numQubits]; diff --git a/python/tests/test_circuit.py b/python/tests/test_circuit.py index d7e16442a..b0d6a5f6f 100644 --- a/python/tests/test_circuit.py +++ b/python/tests/test_circuit.py @@ -128,7 +128,7 @@ def test_get_qsharp_circuit_prune_classical_qubits(self): state_prep_params = { "rowMap": [1, 0], "stateVector": [0.6, 0.0, 0.0, 0.8], - "expansionOps": [[2]], + "expansionOps": [{"name": "X", "qubits": [2], "controlState": 0, "lookupData": []}], "numQubits": 4, } qsharp_factory = QsharpFactoryData( diff --git a/python/tests/test_state_preparation.py b/python/tests/test_state_preparation.py index 491ec2df1..b8ec42d70 100644 --- a/python/tests/test_state_preparation.py +++ b/python/tests/test_state_preparation.py @@ -362,19 +362,19 @@ def test_find_pivot_row(): m = np.array([[0, 1, 0], [1, 0, 1], [0, 0, 1], [0, 0, 0]], dtype=np.int8) # Test finding pivot in column 0 starting from row 0 - pivot = _find_pivot_row(m, 0, 4, 0) + pivot = _find_pivot_row(m, 0, 0) assert pivot == 1 # Row 1 has a 1 in column 0 # Test finding pivot in column 1 starting from row 0 - pivot = _find_pivot_row(m, 0, 4, 1) + pivot = _find_pivot_row(m, 0, 1) assert pivot == 0 # Row 0 has a 1 in column 1 # Test finding pivot in column 2 starting from row 1 - pivot = _find_pivot_row(m, 1, 4, 2) + pivot = _find_pivot_row(m, 1, 2) assert pivot == 1 # Row 1 has a 1 in column 2 # Test no pivot found - pivot = _find_pivot_row(m, 3, 4, 0) + pivot = _find_pivot_row(m, 3, 0) assert pivot is None # No 1 found in column 0 starting from row 3 @@ -386,8 +386,9 @@ def test_eliminate_column() -> None: row_map = [0, 1, 2, 3] cnot_ops: list[tuple[int, int]] = [] - # Eliminate column 0 with pivot row 0 - m_result, cnot_ops_result = _eliminate_column(matrix, 4, 0, 0, row_map, cnot_ops) + # Eliminate column 0 with pivot row 0 (in-place) + matrix_work = matrix.copy() + _eliminate_column(matrix_work, 0, 0, row_map, cnot_ops) # Verify matrix is modified correctly (rows 1 and 3 should be XORed with row 0) expected = np.array( @@ -400,14 +401,13 @@ def test_eliminate_column() -> None: dtype=np.int8, ) - assert np.array_equal(m_result, expected) + assert np.array_equal(matrix_work, expected) - # Verify CNOT operations are recorded correctly + # Verify CX operations are recorded correctly expected_cnots = [(1, 0), (3, 0)] # (target, control) pairs - assert cnot_ops_result == expected_cnots + assert cnot_ops == expected_cnots - # Verify original inputs are not modified - assert cnot_ops == [] # Original list should be empty + # Verify original matrix is not modified original_expected = np.array([[1, 1, 0], [1, 0, 1], [0, 1, 1], [1, 1, 0]], dtype=np.int8) assert np.array_equal(matrix, original_expected) @@ -417,12 +417,11 @@ def test_perform_gaussian_elimination() -> None: # Test matrix for Gaussian elimination matrix = np.array([[1, 1, 0], [0, 1, 1], [1, 0, 1]], dtype=np.int8) - m, n = matrix.shape row_map = [0, 1, 2] cnot_ops: list[tuple[int, int]] = [] # Perform Gaussian elimination - m_result, row_map_result, cnot_ops_result = _perform_gaussian_elimination(matrix, m, n, row_map, cnot_ops) + m_result, row_map_result, cnot_ops_result = _perform_gaussian_elimination(matrix, row_map, cnot_ops) # Verify the result is in row echelon form # After elimination, we should have: @@ -434,15 +433,15 @@ def test_perform_gaussian_elimination() -> None: assert len(row_map_result) == 3 assert isinstance(cnot_ops_result, list) - # Assert the specific CNOT sequence: CNOT(0,2), CNOT(0,1), CNOT(2,1) + # Assert the specific CX sequence: CX(0,2), CX(0,1), CX(2,1) # For matrix [[1,1,0], [0,1,1], [1,0,1]], Gaussian elimination should produce: - # Column 0: CNOT(2,0) to eliminate position [2,0] - # Column 1: CNOT(0,1) to eliminate position [0,1] - # Column 1: CNOT(2,1) to eliminate position [2,1] (redundant but part of algorithm) - assert len(cnot_ops_result) == 3, f"Expected 3 CNOT operations, got {len(cnot_ops_result)}" - assert cnot_ops_result[0] == (2, 0), f"First CNOT should be CNOT(0,2), got {cnot_ops_result[0]}" - assert cnot_ops_result[1] == (0, 1), f"Second CNOT should be CNOT(0,1), got {cnot_ops_result[1]}" - assert cnot_ops_result[2] == (2, 1), f"Third CNOT should be CNOT(2,1), got {cnot_ops_result[2]}" + # Column 0: CX(2,0) to eliminate position [2,0] + # Column 1: CX(0,1) to eliminate position [0,1] + # Column 1: CX(2,1) to eliminate position [2,1] (redundant but part of algorithm) + assert len(cnot_ops_result) == 3, f"Expected 3 CX operations, got {len(cnot_ops_result)}" + assert cnot_ops_result[0] == (2, 0), f"First CX should be CX(0,2), got {cnot_ops_result[0]}" + assert cnot_ops_result[1] == (0, 1), f"Second CX should be CX(0,1), got {cnot_ops_result[1]}" + assert cnot_ops_result[2] == (2, 1), f"Third CX should be CX(2,1), got {cnot_ops_result[2]}" # Verify original inputs are not modified original_expected = np.array([[1, 1, 0], [0, 1, 1], [1, 0, 1]], dtype=np.int8) @@ -529,8 +528,8 @@ def test_gf2x_with_tracking_basic(): for op in elimination_results.operations: assert isinstance(op, tuple) assert len(op) == 2 - assert op[0] in ["cnot", "x"] - if op[0] == "cnot": + assert op[0] in ["cx", "x"] + if op[0] == "cx": assert isinstance(op[1], tuple) assert len(op[1]) == 2 elif op[0] == "x": @@ -552,9 +551,9 @@ def test_gf2x_with_tracking_duplicate_rows(): elimination_results = gf2x_with_tracking(matrix) - # Should have CNOT operations to eliminate duplicates - cnot_ops = [op for op in elimination_results.operations if op[0] == "cnot"] - assert len(cnot_ops) > 0, "Expected CNOT operations for duplicate elimination" + # Should have CX operations to eliminate duplicates + cnot_ops = [op for op in elimination_results.operations if op[0] == "cx"] + assert len(cnot_ops) > 0, "Expected CX operations for duplicate elimination" # Verify rank reduction due to duplicate elimination original_rank = np.linalg.matrix_rank(matrix) @@ -562,7 +561,7 @@ def test_gf2x_with_tracking_duplicate_rows(): # Verify operations use original matrix indices for op in elimination_results.operations: - if op[0] == "cnot": + if op[0] == "cx": target, control = op[1] assert 0 <= target < matrix.shape[0] assert 0 <= control < matrix.shape[0] @@ -605,13 +604,13 @@ def test_gf2x_with_tracking_diagonal_matrix(): # This should reduce rank from 3 to 2 assert elimination_results.rank == 2, f"Expected rank 2 for diagonal reduction, got {elimination_results.rank}" - # Should have CNOT and X operations from diagonal reduction - cnot_ops = [op for op in elimination_results.operations if op[0] == "cnot"] + # Should have CX and X operations from diagonal reduction + cnot_ops = [op for op in elimination_results.operations if op[0] == "cx"] x_ops = [op for op in elimination_results.operations if op[0] == "x"] - # Diagonal reduction uses CNOT(i, i+1) for i=0 to rank-2, then X on last row - # For 3x3: CNOT(0,1), CNOT(1,2), X(2) - assert len(cnot_ops) >= 2, f"Expected at least 2 CNOT operations, got {len(cnot_ops)}" + # Diagonal reduction uses CX(i, i+1) for i=0 to rank-2, then X on last row + # For 3x3: CX(0,1), CX(1,2), X(2) + assert len(cnot_ops) >= 2, f"Expected at least 2 CX operations, got {len(cnot_ops)}" assert len(x_ops) >= 1, f"Expected at least 1 X operation, got {len(x_ops)}" @@ -688,7 +687,7 @@ def test_gf2x_with_tracking_reconstruction(): # Verify operations use valid indices for op in elimination_results.operations: - if op[0] == "cnot": + if op[0] == "cx": target, control = op[1] assert 0 <= target < original_matrix.shape[0] assert 0 <= control < original_matrix.shape[0] @@ -712,7 +711,7 @@ def test_gf2x_with_tracking_reconstruction(): # Apply operations in reverse order to reconstruct for op in reversed(elimination_results.operations): - if op[0] == "cnot": + if op[0] == "cx": target, control = op[1] reconstructed[target] = reconstructed[target] ^ reconstructed[control] elif op[0] == "x": @@ -745,9 +744,9 @@ def test_remove_duplicate_rows_with_cnot() -> None: # Should have eliminated duplicate rows assert m_result.shape[0] < matrix.shape[0], "Expected rows to be eliminated" - # Should have CNOT operations - cnot_ops = [op for op in operations_result if op[0] == "cnot"] - assert len(cnot_ops) > 0, "Expected CNOT operations for duplicate elimination" + # Should have CX operations + cnot_ops = [op for op in operations_result if op[0] == "cx"] + assert len(cnot_ops) > 0, "Expected CX operations for duplicate elimination" # Verify row mapping consistency assert len(row_map_result) == m_result.shape[0] @@ -862,17 +861,17 @@ def test_reduce_diagonal_matrix() -> None: f"Expected shape (2, 3), got {elimination_results.reduced_matrix.shape}" ) - # Should have CNOT and X operations - cnot_ops = [op for op in elimination_results.operations if op[0] == "cnot"] + # Should have CX and X operations + cnot_ops = [op for op in elimination_results.operations if op[0] == "cx"] x_ops = [op for op in elimination_results.operations if op[0] == "x"] - # For 3x3 diagonal matrix: CNOT(0,2), CNOT(1,2), X(2) - assert len(cnot_ops) == 2, f"Expected 2 CNOT operations, got {len(cnot_ops)}" + # For 3x3 diagonal matrix: CX(0,2), CX(1,2), X(2) + assert len(cnot_ops) == 2, f"Expected 2 CX operations, got {len(cnot_ops)}" assert len(x_ops) == 1, f"Expected 1 X operation, got {len(x_ops)}" - # Assert exact CNOT sequence: CNOT(1,0), CNOT(2,1) - assert cnot_ops[0] == ("cnot", (1, 0)), f"First CNOT should be CNOT(0,1), got {cnot_ops[0]}" - assert cnot_ops[1] == ("cnot", (2, 1)), f"Second CNOT should be CNOT(1,2), got {cnot_ops[1]}" + # Assert exact CX sequence: CX(1,0), CX(2,1) + assert cnot_ops[0] == ("cx", (1, 0)), f"First CX should be CX(0,1), got {cnot_ops[0]}" + assert cnot_ops[1] == ("cx", (2, 1)), f"Second CX should be CX(1,2), got {cnot_ops[1]}" # Verify specific X operation: X(2) - the last row gets X operation expected_x_qubits = {2} @@ -945,10 +944,10 @@ def test_gf2x_with_tracking_edge_case_pseudo_diagonal(): f"Row map length should be {elimination_results.rank}, got {len(elimination_results.row_map)}" ) - # Verify that operations list contains both CNOT and X operations - cnot_ops = [op for op in elimination_results.operations if op[0] == "cnot"] + # Verify that operations list contains both CX and X operations + cnot_ops = [op for op in elimination_results.operations if op[0] == "cx"] x_ops = [op for op in elimination_results.operations if op[0] == "x"] - assert len(cnot_ops) > 0, "Should have recorded some CNOT operations" + assert len(cnot_ops) > 0, "Should have recorded some CX operations" assert len(x_ops) > 0, "Should have recorded some X operations" # Should have some operations recorded From 3102e9be9725b5871c38ec5e13ba7de50de80ad7 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Fri, 3 Apr 2026 19:24:07 +0000 Subject: [PATCH 02/21] add test for binary encoding --- examples/state_prep_energy.ipynb | 2 +- .../state_preparation/sparse_isometry.py | 9 + .../sparse_isometry_binary_encoding.py | 32 +- .../qdk_chemistry/utils/binary_encoding.py | 317 +- .../qdk_chemistry/utils/qsharp/__init__.py | 7 +- .../utils/qsharp/src/BinaryEncoding.qs | 27 +- ...e_sparse_ci_wavefunction.wavefunction.json | 6063 +++++++++++++++++ python/tests/test_state_preparation.py | 245 +- .../test_state_preparation_binary_encoding.py | 329 + python/tests/test_utils_binary_encoding.py | 683 ++ 10 files changed, 7571 insertions(+), 143 deletions(-) create mode 100644 python/tests/test_data/ozone_sparse_ci_wavefunction.wavefunction.json create mode 100644 python/tests/test_state_preparation_binary_encoding.py create mode 100644 python/tests/test_utils_binary_encoding.py diff --git a/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index 968f45b8e..4b6fa7d4c 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -974,4 +974,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index 84dd7e2fa..615366e6c 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -482,6 +482,15 @@ def to_dict(self) -> dict[str, Any]: "lookupData": self.lookup_data, } + def to_qsharp_parameter(self) -> QSHARP_UTILS.BinaryEncoding.MatrixCompressionOp: + """Convert to a Q# MatrixCompressionOp struct.""" + return QSHARP_UTILS.BinaryEncoding.MatrixCompressionOp( + name=self.name, + qubits=self.qubits, + controlState=self.control_state, + lookupData=self.lookup_data, + ) + def gf2x_with_tracking( matrix: np.ndarray, diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index 4947c331a..4ed977835 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -7,11 +7,6 @@ RREF matrix directly into the binary-encoding solver which synthesises the full circuit using batched Toffoli gates and Partial Unary Iteration (PUI) lookup blocks. - -The approach is particularly effective for wavefunctions with many -determinants whose binary matrix has favourable sparsity structure, -since the entire expansion is expressed in terms of CX, Toffoli, and -lookup gates with no intermediate dense state preparation. """ # -------------------------------------------------------------------------------------------- @@ -22,7 +17,6 @@ from typing import Any import numpy as np -import qdk from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.data.circuit import QsharpFactoryData @@ -128,7 +122,7 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, staircase_mode=True) # Step 2: Binary encoding on the reduced RREF matrix - binary_ops, num_ancilla, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) + binary_ops, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) # Step 2b: Build compressed statevector reindexed by the bijection. # The bijection maps (dense_val, orig_col) where orig_col is the @@ -180,33 +174,34 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: # PreparePureStateD treats qubits[0] as MSB, so pass dense_row_map # as-is (row 0 first) — do NOT reverse like the parent sparse isometry # (which uses the opposite convention: row rank-1 = MSB). - state_prep_params = qdk.code.BinaryEncodingStatePreparationParams( + state_prep_params = QSHARP_UTILS.BinaryEncoding.BinaryEncodingStatePreparationParams( rowMap=list(dense_row_map), stateVector=compressed_sv.tolist(), - expansionOps=[op.to_dict() for op in expansion_ops], - binaryEncodingOps=[op.to_dict() for op in encoded_ops], + expansionOps=[op.to_qsharp_parameter() for op in expansion_ops], + binaryEncodingOps=[op.to_qsharp_parameter() for op in encoded_ops], numQubits=n_qubits, - numAncilla=num_ancilla, + numAncilla=0, ) qsharp_factory = QsharpFactoryData( program=QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationCircuit, parameter=vars(state_prep_params), ) - + qsharp_op = QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationOp(*vars(state_prep_params).values()) Logger.info( f"Binary encoding produced {len(binary_ops)} operations ({len(encoded_ops)} encoded) " - f"using {num_ancilla} ancillae for {n_qubits}-qubit system with {len(bitstrings)} determinants" + f"for {n_qubits}-qubit system with {len(bitstrings)} determinants" ) return Circuit( qsharp_factory=qsharp_factory, + qsharp_op=qsharp_op, encoding="jordan-wigner", ) def _perform_binary_encoding( self, gf2x_result: GF2XEliminationResult, n_qubits: int - ) -> tuple[list[tuple[str, Any]], int, list[tuple[int, int]], int]: + ) -> tuple[list[tuple[str, Any]], list[tuple[int, int]], int]: """Run binary-encoding synthesis on the reduced RREF matrix. Args: @@ -214,10 +209,9 @@ def _perform_binary_encoding( n_qubits: Total number of qubits in the original space. Returns: - Tuple of ``(gf2x_ops, num_ancilla, bijection, dense_size)``: + Tuple of ``(gf2x_ops, bijection, dense_size)``: - ``gf2x_ops``: gate operations from the binary-encoding solver. - - ``num_ancilla``: number of ancilla qubits consumed. - ``bijection``: list of ``(dense_val, orig_col)`` mapping each original matrix column to its compressed binary-register label. - ``dense_size``: number of qubits in the compressed dense register. @@ -236,7 +230,7 @@ def _perform_binary_encoding( measurement_based_uncompute=self._settings.get("measurement_based_uncompute"), ) - gf2x_ops, num_ancilla = synthesizer.to_gf2x_operations( + gf2x_ops = synthesizer.to_gf2x_operations( num_local_qubits=n_qubits, active_qubit_indices=gf2x_result.row_map, ancilla_start=n_qubits, @@ -244,11 +238,11 @@ def _perform_binary_encoding( Logger.debug( f"Binary encoding output: {len(gf2x_ops)} ops, " - f"{num_ancilla} ancillae, bijection size {len(synthesizer.bijection)}, " + f"bijection size {len(synthesizer.bijection)}, " f"dense_size {synthesizer.dense_size}" ) - return gf2x_ops, num_ancilla, synthesizer.bijection, synthesizer.dense_size + return gf2x_ops, synthesizer.bijection, synthesizer.dense_size def name(self) -> str: """Return the algorithm identifier string.""" diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index 757485907..f4572494c 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -5,16 +5,6 @@ it synthesises a circuit that compresses the sparse rows into a dense binary-counter register using batched Toffoli gates and Partial Unary Iteration (PUI) lookup blocks. - -Public API ----------- -:class:`BinaryEncodingSynthesizer` - Main facade. Use :meth:`BinaryEncodingSynthesizer.from_matrix` to - create a solved instance, then call - :meth:`~BinaryEncodingSynthesizer.to_gf2x_operations` to export - the resulting gate sequence. -:class:`RrefTableau` - Mutable binary tableau with RREF validation and gate-level updates. """ # -------------------------------------------------------------------------------------------- @@ -46,17 +36,43 @@ def _dense_qubits_size(num_cols: int) -> int: - """Return the dense-register width required to index ``num_cols`` columns.""" + """Return the dense-register width required to index ``num_cols`` columns. + + Args: + num_cols: Number of columns to index. + + Returns: + Number of qubits needed in the dense register to uniquely index all + columns. + + """ return 1 if num_cols < 2 else math.ceil(math.log2(num_cols)) def _int_to_bits(val: int, nbits: int) -> list[bool]: - """Convert an integer to a fixed-width MSB-first bit sequence.""" + """Convert an integer to a fixed-width MSB-first bit sequence. + + Args: + val: Integer value to convert. + nbits: Number of bits in the output sequence. + + Returns: + List of booleans representing the bits of *val*, with the most significant bit first. + + """ return [bool((val >> i) & 1) for i in range(nbits - 1, -1, -1)] def _bits_to_int(bits: Iterable[int | bool]) -> int: - """Convert an MSB-first bit sequence to integer.""" + """Convert an MSB-first bit sequence to integer. + + Args: + bits: Iterable of bits (as integers or booleans), with the most significant bit first. + + Returns: + Integer value represented by the bit sequence. + + """ return sum(int(b) << i for i, b in enumerate(reversed(list(bits)))) @@ -77,11 +93,12 @@ class MatrixCompressionType(Enum): def _is_diagonal_reduction_shape(data: np.ndarray) -> bool: """Return True when the pivot sub-matrix is already upper-staircase. - The filled-REF pattern produced by ``_ref_to_staircase`` has pivot - columns forming ``np.triu(ones(r, r))`` — upper triangular with 1s on - and above the diagonal. When this is detected, binary encoding can - skip its own CX cascade in ``_apply_unary_staircase`` (only X on the - first row is needed). + Args: + data: Binary matrix to check. + + Returns: + True if the pivot sub-matrix is upper-triangular with all 1s, False otherwise. + """ row_norms = np.any(data, axis=1) effective_rows = int(row_norms.sum()) if row_norms.any() else 0 @@ -103,7 +120,15 @@ def _is_diagonal_reduction_shape(data: np.ndarray) -> bool: def _check_rref(data: np.ndarray) -> None: - """Validate that a binary matrix is in reduced row echelon form.""" + """Validate that a binary matrix is in reduced row echelon form. + + Args: + data: Binary matrix to validate. + + Raises: + NotRrefError: If *data* is not in RREF. + + """ num_rows, _ = data.shape prev_pivot = -1 found_zero_row = False @@ -166,19 +191,49 @@ def __init__(self, data: np.ndarray): Logger.debug(f"Tableau shape: {self.data.shape}, dense size: {self.dense_size}, pivots: {self.pivots}") def get(self, row: int, col: int) -> bool: - """Return the value at ``(row, col)``.""" + """Return the value at ``(row, col)``. + + Args: + row: Row index. + col: Column index. + + Returns: + Boolean value at the specified position in the tableau. + + """ return bool(self.data[row, col]) def get_col(self, col: int) -> np.ndarray: - """Return column *col* as a 1-D array.""" + """Return column *col* as a 1-D array. + + Args: + col: Column index. + + Returns: + 1-D array representing the specified column. + + """ return self.data[:, col] def row_is_zero(self, row: int) -> bool: - """Return True if *row* is all zeros.""" + """Return True if *row* is all zeros. + + Args: + row: Row index. + + Returns: + True if the specified row is all zeros, False otherwise. + + """ return not np.any(self.data[row]) def identify_rref_pivots(self) -> list[tuple[int, int]]: - """Find pivot positions using vectorized operations.""" + """Find pivot positions using vectorized operations. + + Returns: + List of pivot positions as (row, col) tuples. + + """ row_indices, col_indices = np.nonzero(self.data) _, first_occurrences = np.unique(row_indices, return_index=True) return list( @@ -190,15 +245,32 @@ def identify_rref_pivots(self) -> list[tuple[int, int]]: ) def cx(self, control: int, target: int): - """Apply CX: ``target ^= control``.""" + """Apply CX: ``target ^= control``. + + Args: + control: Control-row index. + target: Target-row index. + + """ self.data[target] ^= self.data[control] def swap(self, a: int, b: int): - """Swap rows *a* and *b*.""" + """Swap rows *a* and *b*. + + Args: + a: First row index. + b: Second row index. + + """ self.data[[a, b]] = self.data[[b, a]] def x(self, row: int): - """Apply bit-flip (X) to every entry in *row*.""" + """Apply bit-flip (X) to every entry in *row*. + + Args: + row: Row index. + + """ self.data[row] ^= 1 def permute_columns(self, col_order: list[int]): @@ -275,7 +347,9 @@ class _BatchElement: """Internal tracking structure for one element within a synthesis batch.""" col: int | None + """Column index of the element's non-zero entry, or None if the batch row is currently zero.""" dense_content: int + """Dense-register content of the batch element.""" class BinaryEncodingSynthesizer: @@ -287,17 +361,6 @@ class BinaryEncodingSynthesizer: a compact binary-counter register via a unary-to-binary ladder. * **Stage 2 — non-pivot processing**: encodes remaining columns using batched Toffoli gates and Partial Unary Iteration (PUI) lookup blocks. - - Typical usage:: - - synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) - ops, n_anc = synth.to_gf2x_operations(num_local_qubits=n) - - Or, for more control:: - - synth = BinaryEncodingSynthesizer(RrefTableau(matrix)) - synth.run() - ops, n_anc = synth.to_gf2x_operations(num_local_qubits=n) """ def __init__( @@ -391,23 +454,28 @@ def max_batch_size(self) -> int: return 1 << (sparse_size.bit_length() - 1) def _record(self, op: tuple[MatrixCompressionType, tuple[Any, ...]]): - """Append an operation and update the tableau.""" + """Append an operation and update the tableau. + + Args: + op: Operation to record, as a tuple of (MatrixCompressionType, payload). + + """ self.circuit.append(op) - kind, payload = op + compress_type, payload = op - if kind is MatrixCompressionType.CX: + if compress_type is MatrixCompressionType.CX: self.tableau.cx(*payload) - elif kind is MatrixCompressionType.SWAP: + elif compress_type is MatrixCompressionType.SWAP: self.tableau.swap(*payload) - elif kind is MatrixCompressionType.TOFFOLI: + elif compress_type is MatrixCompressionType.TOFFOLI: tgt, ctrl_pos, ctrl_row, ctrl_val = payload self.tableau.toffoli(tgt, (ctrl_pos, True), (ctrl_row, ctrl_val)) - elif kind is MatrixCompressionType.PUI_BLOCK: + elif compress_type is MatrixCompressionType.PUI_BLOCK: fixed_controls, rest_entries = payload self.tableau.toffoli_pui_fixed(fixed_controls) for off, ctrls in rest_entries: self.tableau.toffoli_pui_rest(off, ctrls) - elif kind is MatrixCompressionType.X: + elif compress_type is MatrixCompressionType.X: self.tableau.x(*payload) def run(self): @@ -468,6 +536,10 @@ def _run_stage1_diagonal_encoding(self, rank: int): 2. Binary compression: A divide-and-conquer loop folds the unary rows into binary-counter dense rows using one Toffoli per erased unary bit, with no ancilla waste. + + Args: + rank: Number of pivot columns (size of the identity block). + """ if rank == 0: return @@ -482,7 +554,18 @@ def _run_stage1_diagonal_encoding(self, rank: int): self.bijection.append((dense_val, c)) def _apply_unary_staircase(self, rank: int) -> list[int]: - """Convert the identity block into an upper-staircase matrix.""" + """Convert the identity block into an upper-staircase matrix. + + For each pivot column c, apply CX from row c into every row above it, + then apply X to row 0 and SWAP to move the pivot row up to row c. + + Args: + rank: Number of pivot columns (size of the identity block). + + Returns: + List of logical row indices corresponding to the original pivot rows. + + """ logical_rows = list(range(rank)) if not self.tableau.is_diagonal_reduced: for i in range(rank - 2, -1, -1): @@ -491,7 +574,16 @@ def _apply_unary_staircase(self, rank: int) -> list[int]: return logical_rows def _convert_unary_to_binary(self, limit: int, logical_rows: list[int]): - """Fold unary rows into binary-counter dense rows.""" + """Fold unary rows into binary-counter dense rows. + + This is a recursive divide-and-conquer process that iteratively folds + unary rows into binary-counter dense rows. + + Args: + limit: Number of unary rows to process (initially the rank). + logical_rows: Current mapping of logical row indices to physical rows. + + """ logical_rows = [*logical_rows[1:], logical_rows[0]] if limit > 1: @@ -550,6 +642,13 @@ def _choose_stage_two_start_index(self, rank: int) -> int: If capacity is insufficient, return ``rank`` and allow Stage Two to flush an early partial batch to reach the next aligned boundary. + + Args: + rank: Number of pivot columns (size of the identity block). + + Returns: + The chosen start index for Stage Two processing. + """ mbs = self.max_batch_size() if mbs <= 1: @@ -573,6 +672,11 @@ def _run_stage2_non_pivot_col_processing(self, k_start: int): ``max_batch_size`` or would cross an alignment boundary, because sparse indicator rows are reused across batches. The final (partial) batch is flushed at the end of the loop, and any remaining edge case is resolved. + + Args: + k_start: Starting index for non-pivot columns to process, + typically chosen to optimize the first batch's PUI cost. + """ mbs = self.max_batch_size() self.batch_index = k_start @@ -764,56 +868,49 @@ def to_gf2x_operations( num_local_qubits: int, active_qubit_indices: list[int] | None = None, ancilla_start: int | None = None, - ) -> tuple[list[tuple[str, Any]], int]: + ) -> list[tuple[str, Any]]: """Translate recorded circuit operations into GF2+X instruction tuples. Args: - num_local_qubits: Index where ancilla allocation can start. + num_local_qubits: Number of local (active) qubits. active_qubit_indices: Optional mapping from local qubit index (0..num_local_qubits-1) to global qubit index. If provided, operations are translated to global indices. ancilla_start: Optional global starting index for ancillas. Used if active_qubit_indices is provided. Returns: - Pair ``(ops, num_ancilla)`` with generated operations (optionally translated) - and the total ancilla count consumed by lookup blocks. + List of generated operations (optionally translated to global indices). """ ops: list[tuple[str, Any]] = [] - start_ancilla_idx = num_local_qubits - max_ancilla_idx = start_ancilla_idx - 1 - for kind, payload in self.circuit: - if kind is MatrixCompressionType.CX: + for compress_type, payload in self.circuit: + if compress_type is MatrixCompressionType.CX: ops.append(("cx", payload)) - elif kind is MatrixCompressionType.SWAP: + elif compress_type is MatrixCompressionType.SWAP: ops.append(("swap", payload)) - elif kind is MatrixCompressionType.TOFFOLI: + elif compress_type is MatrixCompressionType.TOFFOLI: target, ctrl_pos, ctrl_row, ctrl_val = payload if not ctrl_val: ops.append(("x", ctrl_row)) ops.append(("ccx", (target, ctrl_pos, ctrl_row))) if not ctrl_val: ops.append(("x", ctrl_row)) - elif kind is MatrixCompressionType.X: + elif compress_type is MatrixCompressionType.X: ops.append(("x", payload[0])) - elif kind is MatrixCompressionType.PUI_BLOCK: + elif compress_type is MatrixCompressionType.PUI_BLOCK: fixed_controls, rest_entries = payload - new_max = self._flush_pui_lookup_block( + self._flush_pui_lookup_block( ops, self.dense_size, fixed_controls, rest_entries, - start_ancilla_idx, ) - max_ancilla_idx = max(max_ancilla_idx, new_max) - - num_ancilla = max(0, max_ancilla_idx - start_ancilla_idx + 1) if active_qubit_indices is not None and ancilla_start is not None: ops = self._translate_ops(ops, num_local_qubits, active_qubit_indices, ancilla_start) - return ops, num_ancilla + return ops @staticmethod def _translate_ops( @@ -886,8 +983,7 @@ def _flush_pui_lookup_block( sbs: int, fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], - start_ancilla_idx: int, - ) -> int: + ) -> None: """Convert one recorded PUI block into lookup-based GF2+X operations. Args: @@ -895,63 +991,58 @@ def _flush_pui_lookup_block( sbs: Dense-register width. fixed_controls: Shared controls for all block entries. rest_entries: Per-target offsets and changing controls. - start_ancilla_idx: First ancilla index available for lookup synth. - - Returns: - Maximum ancilla index used by this block, or - ``start_ancilla_idx - 1`` when no ancilla is required. """ if not rest_entries: - return start_ancilla_idx - 1 + return - mono_ops, mono_max, mono_and = self._synthesize_single_pui_lookup_block( + mono_ops, mono_and = self._synthesize_single_pui_lookup_block( sbs, fixed_controls, rest_entries, - start_ancilla_idx, ) chunked = self._split_rest_entries_into_power_of_two_chunks(rest_entries) if len(chunked) <= 1: ops.extend(mono_ops) - return mono_max + return - chunked_ops, chunked_max, chunked_and = [], start_ancilla_idx - 1, 0 + chunked_ops, chunked_and = [], 0 for chunk in chunked: - sub_ops, sub_max, sub_and = self._synthesize_single_pui_lookup_block( + sub_ops, sub_and = self._synthesize_single_pui_lookup_block( sbs, fixed_controls, chunk, - start_ancilla_idx, ) chunked_ops.extend(sub_ops) - chunked_max = max(chunked_max, sub_max) chunked_and += sub_and if chunked_and <= mono_and: ops.extend(chunked_ops) - return chunked_max + return ops.extend(mono_ops) - return mono_max def _synthesize_single_pui_lookup_block( self, sbs: int, fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], - start_ancilla_idx: int, - ) -> tuple[list[tuple[str, Any]], int, int]: + ) -> tuple[list[tuple[str, Any]], int]: """Lower one PUI sub-block into lookup ops. + Args: + sbs: Dense-register width. + fixed_controls: Shared controls for all block entries. + rest_entries: Per-target offsets and changing controls. + Returns: - ``(ops, max_ancilla_idx, and_count)`` where ``and_count`` is the + ``(ops, and_count)`` where ``and_count`` is the number of emitted ``and`` operations, used as a Toffoli-cost proxy. """ if not rest_entries: - return [], start_ancilla_idx - 1, 0 + return [], 0 fixed_controls, rest_entries = self._canonicalize_pui_controls(fixed_controls, rest_entries) address_qubits = self._collect_pui_address_qubits(fixed_controls, rest_entries) @@ -959,9 +1050,9 @@ def _synthesize_single_pui_lookup_block( filtered_table = self._build_pui_lookup_table(fixed_controls, rest_entries, address_qubits) if not filtered_table: - return [], start_ancilla_idx - 1, 0 + return [], 0 - lookup_ops, num_ancilla_used = _lookup_select( + lookup_ops = _lookup_select( filtered_table, address_qubits=address_qubits, data_qubits=data_qubits, @@ -972,8 +1063,7 @@ def _synthesize_single_pui_lookup_block( gf2x_ops = list(reversed(typed_lookup_ops)) and_count = sum(1 for name, _ in typed_lookup_ops if name == "and") - max_ancilla = start_ancilla_idx + num_ancilla_used - 1 if num_ancilla_used > 0 else start_ancilla_idx - 1 - return gf2x_ops, max_ancilla, and_count + return gf2x_ops, and_count def _canonicalize_pui_controls( self, @@ -984,6 +1074,17 @@ def _canonicalize_pui_controls( For a given block, rows that appear in every entry with the same value do not need to remain in per-entry changing controls. + + Args: + fixed_controls: Initial list of fixed controls, as (row, value) pairs. + rest_entries: List of (offset, changing_controls) where changing_controls is a list of + (row, value) pairs that may differ between entries. + + Returns: + Tuple of (new_fixed_controls, new_rest_entries) + where new_fixed_controls is the updated list of fixed controls and new_rest_entries + is the updated list of entries with promoted controls removed from changing_controls. + """ if not rest_entries: return fixed_controls, rest_entries @@ -1017,6 +1118,15 @@ def _split_rest_entries_into_power_of_two_chunks( This keeps control patterns local while converting expensive non-power-of-two lookup tables into cheaper composable pieces. + + Args: + rest_entries: List of (offset, changing_controls) where changing_controls is a list of + (row, value) pairs that may differ between entries. + + Returns: + List of chunks, where each chunk is a contiguous sublist of rest_entries with length that is a power of two. + The original order of entries is preserved. + """ n = len(rest_entries) if n <= 2: @@ -1035,7 +1145,17 @@ def _collect_pui_address_qubits( fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], ) -> list[int]: - """Collect and sort all control rows that address a PUI lookup table.""" + """Collect and sort all control rows that address a PUI lookup table. + + Args: + fixed_controls: List of fixed controls, as (row, value) pairs. + rest_entries: List of (offset, changing_controls) where changing_controls is a list of + (row, value) pairs that may differ between entries. + + Returns: + Sorted list of all control rows that address the PUI lookup table. + + """ all_ctrl_rows = {row for row, _ in fixed_controls} for _, changing_controls in rest_entries: all_ctrl_rows.update(row for row, _ in changing_controls) @@ -1049,6 +1169,12 @@ def _build_pui_lookup_table( ) -> dict[tuple[int, ...], tuple[int, ...]]: """Build sparse truth table for one PUI lookup block. + Args: + fixed_controls: List of fixed controls, as (row, value) pairs. + rest_entries: List of (offset, changing_controls) where changing_controls is a list of + (row, value) pairs that may differ between entries. + address_qubits: List of control rows that will serve as address bits for the lookup. + Returns: Mapping from address bit tuples to one-hot output tuples, with all-zero outputs omitted. @@ -1071,7 +1197,7 @@ def _lookup_select( data_qubits: list[int], *, use_measurement_and: bool = False, -) -> tuple[list[tuple[str, Any]], int]: +) -> list[tuple[str, Any]]: """Synthesize a lookup-based select or select_and operation for a given truth table. Args: @@ -1084,12 +1210,11 @@ def _lookup_select( The choice affects the number of ancillas used and the structure of the emitted operations. Returns: - Tuple ``(ops, num_ancilla_used)`` where ``ops`` is a list of GF2+X operations implementing the lookup, - and ``num_ancilla_used`` is the number of ancilla qubits consumed. + List of GF2+X operations implementing the lookup. """ if not table_dict: - return [], 0 + return [] operations: list[tuple[str, Any]] = [] @@ -1109,6 +1234,4 @@ def _lookup_select( op_name = "select_and" if use_measurement_and else "select" operations.append((op_name, (data_table, list(address_qubits), list(data_qubits)))) - # Select uses no ancilla qubits (managed internally by Q#). - num_ancilla_used = 0 - return operations, num_ancilla_used + return operations diff --git a/python/src/qdk_chemistry/utils/qsharp/__init__.py b/python/src/qdk_chemistry/utils/qsharp/__init__.py index a2ddf7301..533106de5 100644 --- a/python/src/qdk_chemistry/utils/qsharp/__init__.py +++ b/python/src/qdk_chemistry/utils/qsharp/__init__.py @@ -18,8 +18,9 @@ # Initialize Q# interpreter qdk_config = get_qdk_profile_config() _QDK_INTERPRETER_PROFILE = qdk_config.get_target_profile() -if _QDK_INTERPRETER_PROFILE == "unrestricted": # Default by Q# if not set - _QDK_INTERPRETER_PROFILE = qsharp.TargetProfile.Adaptive_RIF +if _QDK_INTERPRETER_PROFILE != "adaptive_rif": # Default by Q# if not set + target_profile = qsharp.TargetProfile.Adaptive_RIF + _QDK_INTERPRETER_PROFILE = str(target_profile) Logger.debug( f"QDK interpreter profile initialized to '{_QDK_INTERPRETER_PROFILE}'. " "If you imported Q# code before this module was loaded, please re-import it, " @@ -27,5 +28,5 @@ ) _QS_DIR = Path(__file__).parent -qdk_init(project_root=_QS_DIR, target_profile=_QDK_INTERPRETER_PROFILE) +qdk_init(project_root=_QS_DIR, target_profile=target_profile) QSHARP_UTILS = qdk.code.QDKChemistry.Utils diff --git a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs index e6eb2b126..806134bba 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs @@ -267,21 +267,13 @@ namespace QDKChemistry.Utils.BinaryEncoding { /// Create a callable for the binary-encoding state preparation. function MakeBinaryEncodingStatePreparationOp( - params : BinaryEncodingStatePreparationParams, - ) : Qubit[] => Unit { - BinaryEncodingStatePreparation(params, _) - } - - /// Top-level circuit entry point for binary-encoding state preparation. - operation MakeBinaryEncodingStatePreparationCircuit( rowMap : Int[], stateVector : Double[], expansionOps : MatrixCompressionOp[], binaryEncodingOps : MatrixCompressionOp[], numQubits : Int, numAncilla : Int, - ) : Unit { - use qs = Qubit[numQubits + numAncilla]; + ) : Qubit[] => Unit { BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { rowMap = rowMap, stateVector = stateVector, @@ -289,24 +281,18 @@ namespace QDKChemistry.Utils.BinaryEncoding { binaryEncodingOps = binaryEncodingOps, numQubits = numQubits, numAncilla = numAncilla, - }, qs); + }, _) } - /// Simulate the binary-encoding state preparation and measure system qubits. - /// - /// This is a verification helper: it runs the full state preparation, - /// measures only the system qubits (not ancilla), resets everything, - /// and returns the measurement results. - import Std.Measurement.MeasureEachZ; - - operation VerifyBinaryEncodingStatePrep( + /// Top-level circuit entry point for binary-encoding state preparation. + operation MakeBinaryEncodingStatePreparationCircuit( rowMap : Int[], stateVector : Double[], expansionOps : MatrixCompressionOp[], binaryEncodingOps : MatrixCompressionOp[], numQubits : Int, numAncilla : Int, - ) : Result[] { + ) : Unit { use qs = Qubit[numQubits + numAncilla]; BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { rowMap = rowMap, @@ -316,8 +302,5 @@ namespace QDKChemistry.Utils.BinaryEncoding { numQubits = numQubits, numAncilla = numAncilla, }, qs); - let results = MeasureEachZ(qs[0..numQubits - 1]); - ResetAll(qs); - return results } } diff --git a/python/tests/test_data/ozone_sparse_ci_wavefunction.wavefunction.json b/python/tests/test_data/ozone_sparse_ci_wavefunction.wavefunction.json new file mode 100644 index 000000000..e704e65b1 --- /dev/null +++ b/python/tests/test_data/ozone_sparse_ci_wavefunction.wavefunction.json @@ -0,0 +1,6063 @@ +{ + "container": { + "coefficients": [ + -0.9375631083969103, + 0.32348519364143224, + 0.09466217151416102, + -0.05969506729549011, + -0.059695067295494224, + 0.015646674831562152 + ], + "configuration_set": { + "configurations": [ + { + "configuration": "22000000" + }, + { + "configuration": "20200000" + }, + { + "configuration": "02200000" + }, + { + "configuration": "u2d00000" + }, + { + "configuration": "d2u00000" + }, + { + "configuration": "02020000" + } + ], + "orbitals": { + "active_space_indices": { + "alpha": [ + 8, + 11, + 12, + 13, + 14 + ], + "beta": [ + 8, + 11, + 12, + 13, + 14 + ] + }, + "ao_overlap": [ + [ + 0.9999999999999997, + -0.21406265175603026, + 0.1943841520528101, + -4.3019621682534965e-16, + 2.389200264606652e-17, + 0.0, + 2.796751450445084e-17, + 0.0, + 0.0, + -4.6317979388087364e-32, + 0.0, + 0.0, + 0.0, + -3.202167508042868e-18, + 0.00010162795342230995, + 0.004080448450362301, + 0.03974650583542918, + 0.009153039498940866, + -0.002223586696745214, + 0.0, + 0.09746786505592943, + -0.02367828174712901, + 0.0, + -0.007576093287140207, + 0.0, + -0.009533863074312589, + 0.0, + 0.014672642770263727, + 7.566554767214532e-08, + 5.069776116527167e-06, + 0.0019456341372848032, + 8.803760609894545e-06, + -8.80376253701205e-06, + 0.0, + 0.007552587781947305, + -0.007552589435186726, + 0.0, + -1.9683332510060828e-05, + 0.0, + -1.1364177323232801e-05, + 0.0, + -4.308623558231127e-12 + ], + [ + -0.21406265175603026, + 1.0, + 0.7086073285770355, + -4.155651732842014e-16, + -1.1770728533217567e-17, + 0.0, + -2.7065171743998327e-17, + 0.0, + 0.0, + 3.8496715794608613e-32, + 0.0, + -1.1102230246251565e-16, + 0.0, + 1.9439942655719799e-16, + 0.004080448450362302, + 0.06811296964237698, + 0.20585498647117845, + 0.10865213780458127, + -0.026395324550183667, + 0.0, + 0.3948033487530427, + -0.09591125158143474, + 0.0, + -0.06285685282841283, + 0.0, + -0.07909995368794032, + 0.0, + 0.12173505687685107, + 5.0697761165271665e-06, + 0.00033853968955121453, + 0.01849270383788614, + 0.0005760544787872634, + -0.0005760546048839077, + 0.0, + 0.05288049622100917, + -0.05288050779639545, + 0.0, + -0.0013191603444238338, + 0.0, + -0.0007616175799573981, + 0.0, + -2.8876031716667657e-10 + ], + [ + 0.1943841520528101, + 0.7086073285770355, + 0.9999999999999999, + 2.482061273469195e-16, + -7.628016632237085e-18, + 0.0, + -4.652547417958139e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -2.0735898152616163e-17, + 0.03974650583542918, + 0.20585498647117842, + 0.4335168746666555, + 0.17958463263778798, + -0.043627256292236995, + 0.0, + 0.5648170246110735, + -0.13721339475981995, + 0.0, + -0.04734638995902655, + 0.0, + -0.05958136757615845, + 0.0, + 0.09169589655259233, + 0.0019456341372848034, + 0.018492703837886128, + 0.0872836987416364, + 0.01785215820259852, + -0.017852162110383842, + 0.0, + 0.15234259356139543, + -0.15234262690874453, + 0.0, + -0.02341578904965525, + 0.0, + -0.01351911211110627, + 0.0, + -5.125647327134479e-09 + ], + [ + -4.3019621682534965e-16, + -4.155651732842014e-16, + 2.482061273469195e-16, + 1.0000000000000004, + 0.0, + 0.0, + 0.5012714832460011, + 3.3213815826479783e-33, + 0.0, + -2.341990219245728e-16, + 0.0, + -5.351273040550136e-16, + 0.0, + 9.268676791406428e-16, + -0.00915303949894087, + -0.10865213780458133, + -0.17958463263778782, + -0.1606684845192459, + 0.0472212098410381, + 0.0, + -0.19312697634579026, + 0.08254384937149045, + 0.0, + 0.104746899952449, + 0.0, + 0.10861891525861594, + 0.0, + -0.15764721779892077, + -8.803760609894546e-06, + -0.0005760544787872634, + -0.017852158202598514, + -0.0008911732623832433, + 0.0010106515112425852, + 0.0, + -0.03595349163552047, + 0.05000229947454265, + 0.0, + 0.002186061505946864, + 0.0, + 0.00126212323357384, + 0.0, + 0.00027417545938382675 + ], + [ + 2.389200264606652e-17, + -1.1770728533217567e-17, + -7.628016632237085e-18, + 0.0, + 1.0000000000000004, + 0.0, + 3.3213815826479783e-33, + 0.5012714832460011, + 0.0, + 9.268676791406428e-16, + 0.0, + 1.3521486835209912e-16, + 0.0, + 2.3419902192457273e-16, + 0.002223586696745215, + 0.02639532455018368, + 0.043627256292236974, + 0.04722120984103809, + 0.022238435160131943, + 0.0, + 0.08254384937149051, + 0.12659886883780622, + 0.0, + 0.05490692431256335, + 0.0, + -0.026387253656220085, + 0.0, + 0.07978776481024859, + 8.803762537012048e-06, + 0.0005760546048839074, + 0.017852162110383842, + 0.0010106515112425852, + -0.0008911737048405266, + 0.0, + 0.05000229947454267, + -0.035953513526232904, + 0.0, + -0.00218606210450154, + 0.0, + -0.0012621235098489321, + 0.0, + 0.0002741744423227452 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + -2.341990219245728e-16, + 0.0, + 9.268676791406428e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.033710083897023825, + 0.0, + 0.0, + 0.14665159617300735, + 0.0, + -0.020744928862580413, + 0.0, + 0.08539318640458382, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00011947802763072454, + 0.0, + 0.0, + 0.014048796893667163, + 0.0, + -0.00027417498086138985, + 0.0, + 0.0002741749208453, + 0.0 + ], + [ + 2.796751450445084e-17, + -2.7065171743998327e-17, + -4.652547417958139e-16, + 0.5012714832460011, + 3.3213815826479783e-33, + 0.0, + 1.0000000000000004, + -5.429335180183616e-32, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.09746786505592943, + -0.3948033487530427, + -0.5648170246110737, + -0.1931269763457908, + 0.08254384937149059, + 0.0, + -0.20436795239280633, + 0.16312700897437696, + 0.0, + 0.04779118054078318, + 0.0, + 0.020052863432843904, + 0.0, + -0.014412762845894475, + -0.007552587781947305, + -0.05288049622100919, + -0.15234259356139546, + -0.0359534916355205, + 0.05000229947454265, + 0.0, + -0.13248420174634648, + 0.24100779719391296, + 0.0, + 0.04470532973944468, + 0.0, + 0.02581063632877248, + 0.0, + 0.017166395198803548 + ], + [ + 0.0, + 0.0, + 0.0, + 3.3213815826479783e-33, + 0.5012714832460011, + 0.0, + -5.429335180183616e-32, + 1.0000000000000007, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.023678281747129014, + 0.09591125158143476, + 0.13721339475981995, + 0.08254384937149059, + 0.12659886883780624, + 0.0, + 0.16312700897437696, + 0.427489190371107, + 0.0, + 0.12725960516354526, + 0.0, + -0.004871527143096022, + 0.0, + 0.07520554244496468, + 0.007552589435186727, + 0.05288050779639545, + 0.1523426269087445, + 0.050002299474542704, + -0.0359535135262329, + 0.0, + 0.241007797193913, + -0.1324843072581417, + 0.0, + -0.044705347040653355, + 0.0, + -0.025810641978645445, + 0.0, + 0.01716637186939859 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1466515961730074, + 0.0, + 0.0, + 0.4671183263621878, + 0.0, + -0.035852094426474676, + 0.0, + 0.14757942062057655, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014048796893667172, + 0.0, + 0.0, + 0.10852354269167469, + 0.0, + -0.017166385412937968, + 0.0, + 0.017166381655267123, + 0.0 + ], + [ + -4.6317979388087364e-32, + 3.8496715794608613e-32, + 0.0, + -2.341990219245728e-16, + 9.268676791406428e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000009, + 0.0, + 0.0, + 0.0, + 0.0, + -0.007576093287140213, + -0.06285685282841283, + -0.04734638995902653, + -0.10474689995244894, + -0.054906924312563284, + 0.0, + -0.04779118054078314, + -0.1272596051635453, + 0.0, + -0.12436670455561938, + 0.0, + 0.041832706136542074, + 0.0, + -0.16526009666727273, + -1.9683332510060828e-05, + -0.0013191603444238338, + -0.023415789049655245, + -0.0021860615059468595, + 0.002186062104501536, + 0.0, + -0.04470532973944464, + 0.044705347040653355, + 0.0, + 0.005168627438572901, + 0.0, + 0.0029433747916532043, + 0.0, + 1.411212555667285e-09 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -2.341990219245728e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020744928862580392, + 0.0, + 0.0, + 0.03585209442647466, + 0.0, + 0.023973832824395986, + 0.0, + 0.056766728472505405, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0002741749808613893, + 0.0, + 0.0, + 0.01716638541293795, + 0.0, + -0.0006038717388673926, + 0.0, + 0.00067442434495015, + 0.0 + ], + [ + 0.0, + -1.1102230246251565e-16, + 0.0, + -5.351273040550136e-16, + 1.3521486835209912e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 5.551115123125783e-17, + -0.009533863074312593, + -0.07909995368794032, + -0.05958136757615842, + -0.10861891525861589, + 0.026387253656220057, + 0.0, + -0.020052863432844015, + 0.0048715271430960355, + 0.0, + 0.04183270613654207, + 0.0, + 0.090407284654967, + 0.0, + -0.08101752842679082, + -1.1364177323232803e-05, + -0.000761617579957398, + -0.01351911211110627, + -0.0012621232335738378, + 0.0012621235098489295, + 0.0, + -0.025810636328772465, + 0.025810641978645438, + 0.0, + 0.0029433747916532047, + 0.0, + 0.0017699109819992547, + 0.0, + 6.442960808156293e-10 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 9.268676791406428e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.08539318640458377, + 0.0, + 0.0, + -0.14757942062057655, + 0.0, + 0.05676672847250539, + 0.0, + -0.19590675643060682, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.0002741749208452994, + 0.0, + 0.0, + -0.017166381655267123, + 0.0, + 0.0006744243449501501, + 0.0, + -0.0006038714436083837, + 0.0 + ], + [ + -3.202167508042868e-18, + 1.9439942655719799e-16, + -2.0735898152616163e-17, + 9.268676791406428e-16, + 2.3419902192457273e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 5.551115123125783e-17, + 0.0, + 1.0000000000000004, + 0.014672642770263734, + 0.12173505687685104, + 0.0916958965525923, + 0.15764721779892069, + -0.07978776481024852, + 0.0, + 0.014412762845894619, + -0.07520554244496469, + 0.0, + -0.16526009666727276, + 0.0, + -0.08101752842679082, + 0.0, + 0.11036239028057575, + -4.30862356602057e-12, + -2.8876031747741167e-10, + -5.125647304242803e-09, + -0.0002741754593838268, + -0.0002741744423227442, + 0.0, + -0.017166395198803478, + -0.017166371869398622, + 0.0, + 1.4112125543662424e-09, + 0.0, + 6.442960795145867e-10, + 0.0, + -0.0012782959361877459 + ], + [ + 0.00010162795342230995, + 0.004080448450362302, + 0.03974650583542918, + -0.00915303949894087, + 0.002223586696745215, + 0.0, + -0.09746786505592943, + 0.023678281747129014, + 0.0, + -0.007576093287140213, + 0.0, + -0.009533863074312593, + 0.0, + 0.014672642770263734, + 0.9999999999999997, + -0.21406265175603026, + 0.1943841520528101, + -1.256820191558359e-18, + -2.2686415810783093e-18, + 0.0, + 8.4325524559358395e-19, + 1.1424697650192384e-18, + 0.0, + 2.3986902530670145e-36, + 0.0, + 0.0, + 0.0, + -3.202167508042868e-18, + 0.00010162693264077538, + 0.004080409564275595, + 0.03974636888228243, + 0.002223570477259914, + -0.0091529550214039, + 0.0, + 0.023678275898299028, + -0.09746765236269586, + 0.0, + -0.0075760350931599885, + 0.0, + -0.009533773448940991, + 0.0, + -0.014672498110219848 + ], + [ + 0.004080448450362301, + 0.06811296964237698, + 0.20585498647117842, + -0.10865213780458133, + 0.02639532455018368, + 0.0, + -0.3948033487530427, + 0.09591125158143476, + 0.0, + -0.06285685282841283, + 0.0, + -0.07909995368794032, + 0.0, + 0.12173505687685104, + -0.21406265175603026, + 1.0, + 0.7086073285770355, + -1.7861408919211106e-17, + -2.224135734905169e-18, + 0.0, + -6.698492263399967e-18, + -9.782925105497325e-19, + 0.0, + -1.148280252259879e-36, + 0.0, + -1.1102230246251565e-16, + 0.0, + 1.9439942655719799e-16, + 0.004080409564275596, + 0.06811257602876829, + 0.20585442866737982, + 0.026395239620186507, + -0.10865157794344339, + 0.0, + 0.09591129111752775, + -0.39480274748264355, + 0.0, + -0.062856713192367, + 0.0, + -0.07909964195679742, + 0.0, + -0.12173452131475754 + ], + [ + 0.03974650583542918, + 0.20585498647117845, + 0.4335168746666555, + -0.17958463263778782, + 0.043627256292236974, + 0.0, + -0.5648170246110737, + 0.13721339475981995, + 0.0, + -0.04734638995902653, + 0.0, + -0.05958136757615842, + 0.0, + 0.0916958965525923, + 0.1943841520528101, + 0.7086073285770355, + 0.9999999999999999, + 3.8782207397956175e-18, + 8.491451356543999e-18, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -2.0735898152616163e-17, + 0.03974636888228243, + 0.20585442866737988, + 0.43351608605850844, + 0.043627257943736626, + -0.17958429190797834, + 0.0, + 0.13721355687414916, + -0.5648165989069881, + 0.0, + -0.04734643717203771, + 0.0, + -0.059581324540096514, + 0.0, + -0.09169578828371144 + ], + [ + 0.009153039498940866, + 0.10865213780458127, + 0.17958463263778798, + -0.1606684845192459, + 0.04722120984103809, + 0.0, + -0.1931269763457908, + 0.08254384937149059, + 0.0, + -0.10474689995244894, + 0.0, + -0.10861891525861589, + 0.0, + 0.15764721779892069, + -1.256820191558359e-18, + -1.7861408919211106e-17, + 3.8782207397956175e-18, + 1.0000000000000004, + 1.8647120475712516e-34, + 0.0, + 0.5012714832460011, + 1.1216163452999718e-34, + 0.0, + 1.766585413836462e-17, + 0.0, + -2.0171518086831035e-18, + 0.0, + 3.493809419218592e-18, + -0.002223570477259915, + -0.0263952396201865, + -0.04362725794373662, + 0.022238222526119452, + 0.047221071647258014, + 0.0, + 0.12659841403330263, + 0.08254394993078457, + 0.0, + -0.05490642258979917, + 0.0, + 0.02638722385097141, + 0.0, + 0.07978753297570615 + ], + [ + -0.002223586696745214, + -0.026395324550183667, + -0.043627256292236995, + 0.0472212098410381, + 0.022238435160131943, + 0.0, + 0.08254384937149059, + 0.12659886883780624, + 0.0, + -0.054906924312563284, + 0.0, + 0.026387253656220057, + 0.0, + -0.07978776481024852, + -2.2686415810783093e-18, + -2.224135734905169e-18, + 8.491451356543999e-18, + 1.8647120475712516e-34, + 1.0000000000000004, + 0.0, + 1.1216163452999718e-34, + 0.5012714832460011, + 0.0, + 3.493809419218593e-18, + 0.0, + -1.0199385642249477e-17, + 0.0, + -1.7665854138364624e-17, + 0.0091529550214039, + 0.10865157794344338, + 0.17958429190797837, + 0.047221071647258014, + -0.16066776351728965, + 0.0, + 0.0825439499307846, + -0.19312712431933293, + 0.0, + -0.10474670063109025, + 0.0, + -0.10861858237355343, + 0.0, + -0.15764665109064105 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.033710083897023825, + 0.0, + 0.0, + 0.1466515961730074, + 0.0, + 0.020744928862580392, + 0.0, + -0.08539318640458377, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + 1.766585413836462e-17, + 0.0, + 3.493809419218593e-18, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03370985989065149, + 0.0, + 0.0, + 0.14665120460348519, + 0.0, + -0.085392674816258, + 0.0, + 0.020744844725191686, + 0.0 + ], + [ + 0.09746786505592943, + 0.3948033487530427, + 0.5648170246110735, + -0.19312697634579026, + 0.08254384937149051, + 0.0, + -0.20436795239280633, + 0.16312700897437696, + 0.0, + -0.04779118054078314, + 0.0, + -0.020052863432844015, + 0.0, + 0.014412762845894619, + 8.4325524559358395e-19, + -6.698492263399967e-18, + 0.0, + 0.5012714832460011, + 1.1216163452999718e-34, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + -8.176263821694028e-18, + 0.0, + 4.720568118420444e-18, + 0.0, + -8.176263821694028e-18, + -0.02367827589829903, + -0.09591129111752777, + -0.13721355687414916, + 0.12659841403330266, + 0.08254394993078457, + 0.0, + 0.42748825110247507, + 0.16312737426079696, + 0.0, + -0.12725926928420325, + 0.0, + 0.0048715839009241535, + 0.0, + 0.0752056271184787 + ], + [ + -0.02367828174712901, + -0.09591125158143474, + -0.13721339475981995, + 0.08254384937149045, + 0.12659886883780622, + 0.0, + 0.16312700897437696, + 0.427489190371107, + 0.0, + -0.1272596051635453, + 0.0, + 0.0048715271430960355, + 0.0, + -0.07520554244496469, + 1.1424697650192384e-18, + -9.782925105497325e-19, + 0.0, + 1.1216163452999718e-34, + 0.5012714832460011, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + -8.176263821694026e-18, + 0.0, + 4.720568118420445e-18, + 0.0, + 8.176263821694026e-18, + 0.09746765236269586, + 0.39480274748264355, + 0.5648165989069881, + 0.08254394993078455, + -0.19312712431933282, + 0.0, + 0.163127374260797, + -0.2043689304271153, + 0.0, + -0.04779135538639326, + 0.0, + -0.0200530582610996, + 0.0, + -0.014413016523404359 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.14665159617300735, + 0.0, + 0.0, + 0.4671183263621878, + 0.0, + 0.03585209442647466, + 0.0, + -0.14757942062057655, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + -8.176263821694026e-18, + 0.0, + -8.176263821694026e-18, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.14665120460348519, + 0.0, + 0.0, + 0.4671175525238211, + 0.0, + -0.14757916851616212, + 0.0, + 0.03585210256182065, + 0.0 + ], + [ + -0.007576093287140207, + -0.06285685282841283, + -0.04734638995902655, + 0.104746899952449, + 0.05490692431256335, + 0.0, + 0.04779118054078318, + 0.12725960516354526, + 0.0, + -0.12436670455561938, + 0.0, + 0.04183270613654207, + 0.0, + -0.16526009666727276, + 2.3986902530670145e-36, + -1.148280252259879e-36, + 0.0, + 1.766585413836462e-17, + 3.493809419218593e-18, + 0.0, + -8.176263821694028e-18, + -8.176263821694026e-18, + 0.0, + 1.0000000000000009, + 0.0, + 0.0, + 0.0, + 0.0, + -0.007576035093159992, + -0.06285671319236702, + -0.047346437172037716, + 0.05490642258979915, + 0.10474670063109019, + 0.0, + 0.1272592692842032, + 0.04779135538639323, + 0.0, + -0.12436569142722652, + 0.0, + 0.04183280451848334, + 0.0, + 0.16525984598628726 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.020744928862580413, + 0.0, + 0.0, + -0.035852094426474676, + 0.0, + 0.023973832824395986, + 0.0, + 0.05676672847250539, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.766585413836462e-17, + 0.0, + 0.0, + -8.176263821694026e-18, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08539267481625794, + 0.0, + 0.0, + 0.14757916851616207, + 0.0, + -0.19590581763200313, + 0.0, + 0.05676654484016897, + 0.0 + ], + [ + -0.009533863074312589, + -0.07909995368794032, + -0.05958136757615845, + 0.10861891525861594, + -0.026387253656220085, + 0.0, + 0.020052863432843904, + -0.004871527143096022, + 0.0, + 0.041832706136542074, + 0.0, + 0.090407284654967, + 0.0, + -0.08101752842679082, + 0.0, + -1.1102230246251565e-16, + 0.0, + -2.0171518086831035e-18, + -1.0199385642249477e-17, + 0.0, + 4.720568118420444e-18, + 4.720568118420445e-18, + 0.0, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 5.551115123125783e-17, + -0.009533773448940994, + -0.07909964195679742, + -0.059581324540096514, + -0.026387223850971392, + 0.10861858237355336, + 0.0, + -0.0048715839009241535, + 0.0200530582610996, + 0.0, + 0.04183280451848335, + 0.0, + 0.09040704865313032, + 0.0, + 0.08101754251333977 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08539318640458382, + 0.0, + 0.0, + 0.14757942062057655, + 0.0, + 0.056766728472505405, + 0.0, + -0.19590675643060682, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 3.493809419218593e-18, + 0.0, + 0.0, + -8.176263821694026e-18, + 0.0, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.020744844725191676, + 0.0, + 0.0, + -0.03585210256182065, + 0.0, + 0.05676654484016896, + 0.0, + 0.02397358145976673, + 0.0 + ], + [ + 0.014672642770263727, + 0.12173505687685107, + 0.09169589655259233, + -0.15764721779892077, + 0.07978776481024859, + 0.0, + -0.014412762845894475, + 0.07520554244496468, + 0.0, + -0.16526009666727273, + 0.0, + -0.08101752842679082, + 0.0, + 0.11036239028057575, + -3.202167508042868e-18, + 1.9439942655719799e-16, + -2.0735898152616163e-17, + 3.493809419218592e-18, + -1.7665854138364624e-17, + 0.0, + -8.176263821694028e-18, + 8.176263821694026e-18, + 0.0, + 0.0, + 0.0, + 5.551115123125783e-17, + 0.0, + 1.0000000000000004, + -0.014672498110219853, + -0.12173452131475757, + -0.0916957882837114, + -0.0797875329757061, + 0.15764665109064097, + 0.0, + -0.0752056271184787, + 0.01441301652340431, + 0.0, + 0.16525984598628726, + 0.0, + 0.08101754251333974, + 0.0, + 0.11036216444476152 + ], + [ + 7.566554767214532e-08, + 5.0697761165271665e-06, + 0.0019456341372848034, + -8.803760609894546e-06, + 8.803762537012048e-06, + 0.0, + -0.007552587781947305, + 0.007552589435186727, + 0.0, + -1.9683332510060828e-05, + 0.0, + -1.1364177323232803e-05, + 0.0, + -4.30862356602057e-12, + 0.00010162693264077538, + 0.004080409564275596, + 0.03974636888228243, + -0.002223570477259915, + 0.0091529550214039, + 0.0, + -0.02367827589829903, + 0.09746765236269586, + 0.0, + -0.007576035093159992, + 0.0, + -0.009533773448940994, + 0.0, + -0.014672498110219853, + 0.9999999999999997, + -0.21406265175603026, + 0.1943841520528101, + 8.085528176774917e-17, + -2.2236428658738885e-16, + 0.0, + -6.994398984838768e-18, + 3.3482025918998453e-17, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -3.202167508042868e-18 + ], + [ + 5.069776116527167e-06, + 0.00033853968955121453, + 0.018492703837886128, + -0.0005760544787872634, + 0.0005760546048839074, + 0.0, + -0.05288049622100919, + 0.05288050779639545, + 0.0, + -0.0013191603444238338, + 0.0, + -0.000761617579957398, + 0.0, + -2.8876031747741167e-10, + 0.004080409564275595, + 0.06811257602876829, + 0.20585442866737988, + -0.0263952396201865, + 0.10865157794344338, + 0.0, + -0.09591129111752777, + 0.39480274748264355, + 0.0, + -0.06285671319236702, + 0.0, + -0.07909964195679742, + 0.0, + -0.12173452131475757, + -0.21406265175603026, + 1.0, + 0.7086073285770355, + 1.2772911192456094e-16, + -5.732480966358953e-16, + 0.0, + 6.767437177045078e-18, + -4.1132040051585347e-16, + 0.0, + 0.0, + 0.0, + -1.1102230246251565e-16, + 0.0, + 1.9439942655719799e-16 + ], + [ + 0.0019456341372848032, + 0.01849270383788614, + 0.0872836987416364, + -0.017852158202598514, + 0.017852162110383842, + 0.0, + -0.15234259356139546, + 0.1523426269087445, + 0.0, + -0.023415789049655245, + 0.0, + -0.01351911211110627, + 0.0, + -5.125647304242803e-09, + 0.03974636888228243, + 0.20585442866737982, + 0.43351608605850844, + -0.04362725794373662, + 0.17958429190797837, + 0.0, + -0.13721355687414916, + 0.5648165989069881, + 0.0, + -0.047346437172037716, + 0.0, + -0.059581324540096514, + 0.0, + -0.0916957882837114, + 0.1943841520528101, + 0.7086073285770355, + 0.9999999999999999, + -7.628016632237085e-18, + 0.0, + 0.0, + 0.0, + -4.652547417958139e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -2.0735898152616163e-17 + ], + [ + 8.803760609894545e-06, + 0.0005760544787872634, + 0.01785215820259852, + -0.0008911732623832433, + 0.0010106515112425852, + 0.0, + -0.0359534916355205, + 0.050002299474542704, + 0.0, + -0.0021860615059468595, + 0.0, + -0.0012621232335738378, + 0.0, + -0.0002741754593838268, + 0.002223570477259914, + 0.026395239620186507, + 0.043627257943736626, + 0.022238222526119452, + 0.047221071647258014, + 0.0, + 0.12659841403330266, + 0.08254394993078455, + 0.0, + 0.05490642258979915, + 0.0, + -0.026387223850971392, + 0.0, + -0.0797875329757061, + 8.085528176774917e-17, + 1.2772911192456094e-16, + -7.628016632237085e-18, + 1.0000000000000004, + -1.1831958984888412e-31, + 0.0, + 0.5012714832460011, + 0.0, + 0.0, + 9.268676791406428e-16, + 0.0, + 1.337818260137534e-16, + 0.0, + -2.317169197851607e-16 + ], + [ + -8.80376253701205e-06, + -0.0005760546048839077, + -0.017852162110383842, + 0.0010106515112425852, + -0.0008911737048405266, + 0.0, + 0.05000229947454265, + -0.0359535135262329, + 0.0, + 0.002186062104501536, + 0.0, + 0.0012621235098489295, + 0.0, + -0.0002741744423227442, + -0.0091529550214039, + -0.10865157794344339, + -0.17958429190797834, + 0.047221071647258014, + -0.16066776351728965, + 0.0, + 0.08254394993078457, + -0.19312712431933282, + 0.0, + 0.10474670063109019, + 0.0, + 0.10861858237355336, + 0.0, + 0.15764665109064097, + -2.2236428658738885e-16, + -5.732480966358953e-16, + 0.0, + -1.1831958984888412e-31, + 1.0000000000000004, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + -2.317169197851607e-16, + 0.0, + -5.351273040550136e-16, + 0.0, + -9.268676791406426e-16 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.00011947802763072454, + 0.0, + 0.0, + 0.014048796893667172, + 0.0, + 0.0002741749808613893, + 0.0, + -0.0002741749208452994, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03370985989065149, + 0.0, + 0.0, + 0.14665120460348519, + 0.0, + 0.08539267481625794, + 0.0, + -0.020744844725191676, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + 9.268676791406428e-16, + 0.0, + -2.317169197851607e-16, + 0.0 + ], + [ + 0.007552587781947305, + 0.05288049622100917, + 0.15234259356139543, + -0.03595349163552047, + 0.05000229947454267, + 0.0, + -0.13248420174634648, + 0.241007797193913, + 0.0, + -0.04470532973944464, + 0.0, + -0.025810636328772465, + 0.0, + -0.017166395198803478, + 0.023678275898299028, + 0.09591129111752775, + 0.13721355687414916, + 0.12659841403330263, + 0.0825439499307846, + 0.0, + 0.42748825110247507, + 0.163127374260797, + 0.0, + 0.1272592692842032, + 0.0, + -0.0048715839009241535, + 0.0, + -0.0752056271184787, + -6.994398984838768e-18, + 6.767437177045078e-18, + 0.0, + 0.5012714832460011, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + -0.007552589435186726, + -0.05288050779639545, + -0.15234262690874453, + 0.05000229947454265, + -0.035953513526232904, + 0.0, + 0.24100779719391296, + -0.1324843072581417, + 0.0, + 0.044705347040653355, + 0.0, + 0.025810641978645438, + 0.0, + -0.017166371869398622, + -0.09746765236269586, + -0.39480274748264355, + -0.5648165989069881, + 0.08254394993078457, + -0.19312712431933293, + 0.0, + 0.16312737426079696, + -0.2043689304271153, + 0.0, + 0.04779135538639323, + 0.0, + 0.0200530582610996, + 0.0, + 0.01441301652340431, + 3.3482025918998453e-17, + -4.1132040051585347e-16, + -4.652547417958139e-16, + 0.0, + 0.5012714832460011, + 0.0, + 0.0, + 1.0000000000000007, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014048796893667163, + 0.0, + 0.0, + 0.10852354269167469, + 0.0, + 0.01716638541293795, + 0.0, + -0.017166381655267123, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.14665120460348519, + 0.0, + 0.0, + 0.4671175525238211, + 0.0, + 0.14757916851616207, + 0.0, + -0.03585210256182065, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5012714832460011, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + -1.9683332510060828e-05, + -0.0013191603444238338, + -0.02341578904965525, + 0.002186061505946864, + -0.00218606210450154, + 0.0, + 0.04470532973944468, + -0.044705347040653355, + 0.0, + 0.005168627438572901, + 0.0, + 0.0029433747916532047, + 0.0, + 1.4112125543662424e-09, + -0.0075760350931599885, + -0.062856713192367, + -0.04734643717203771, + -0.05490642258979917, + -0.10474670063109025, + 0.0, + -0.12725926928420325, + -0.04779135538639326, + 0.0, + -0.12436569142722652, + 0.0, + 0.04183280451848335, + 0.0, + 0.16525984598628726, + 0.0, + 0.0, + 0.0, + 9.268676791406428e-16, + -2.317169197851607e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000009, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.00027417498086138985, + 0.0, + 0.0, + -0.017166385412937968, + 0.0, + -0.0006038717388673926, + 0.0, + 0.0006744243449501501, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.085392674816258, + 0.0, + 0.0, + -0.14757916851616212, + 0.0, + -0.19590581763200313, + 0.0, + 0.05676654484016896, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 9.268676791406428e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 0.0, + 0.0 + ], + [ + -1.1364177323232801e-05, + -0.0007616175799573981, + -0.01351911211110627, + 0.00126212323357384, + -0.0012621235098489321, + 0.0, + 0.02581063632877248, + -0.025810641978645445, + 0.0, + 0.0029433747916532043, + 0.0, + 0.0017699109819992547, + 0.0, + 6.442960795145867e-10, + -0.009533773448940991, + -0.07909964195679742, + -0.059581324540096514, + 0.02638722385097141, + -0.10861858237355343, + 0.0, + 0.0048715839009241535, + -0.0200530582610996, + 0.0, + 0.04183280451848334, + 0.0, + 0.09040704865313032, + 0.0, + 0.08101754251333974, + 0.0, + -1.1102230246251565e-16, + 0.0, + 1.337818260137534e-16, + -5.351273040550136e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000004, + 0.0, + 5.551115123125783e-17 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0002741749208453, + 0.0, + 0.0, + 0.017166381655267123, + 0.0, + 0.00067442434495015, + 0.0, + -0.0006038714436083837, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.020744844725191686, + 0.0, + 0.0, + 0.03585210256182065, + 0.0, + 0.05676654484016897, + 0.0, + 0.02397358145976673, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -2.317169197851607e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0000000000000007, + 0.0 + ], + [ + -4.308623558231127e-12, + -2.8876031716667657e-10, + -5.125647327134479e-09, + 0.00027417545938382675, + 0.0002741744423227452, + 0.0, + 0.017166395198803548, + 0.01716637186939859, + 0.0, + 1.411212555667285e-09, + 0.0, + 6.442960808156293e-10, + 0.0, + -0.0012782959361877459, + -0.014672498110219848, + -0.12173452131475754, + -0.09169578828371144, + 0.07978753297570615, + -0.15764665109064105, + 0.0, + 0.0752056271184787, + -0.014413016523404359, + 0.0, + 0.16525984598628726, + 0.0, + 0.08101754251333977, + 0.0, + 0.11036216444476152, + -3.202167508042868e-18, + 1.9439942655719799e-16, + -2.0735898152616163e-17, + -2.317169197851607e-16, + -9.268676791406426e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 5.551115123125783e-17, + 0.0, + 1.0000000000000004 + ] + ], + "basis_set": { + "atomic_orbital_type": "spherical", + "atoms": [ + { + "atom_index": 0, + "shells": [ + { + "coefficients": [ + 0.00071, + 0.00547, + 0.027837, + 0.1048, + 0.283062, + 0.448719, + 0.270952, + 0.015458 + ], + "exponents": [ + 11720.0, + 1759.0, + 400.8, + 113.7, + 37.03, + 13.27, + 5.025, + 1.013 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + -0.00016, + -0.001263, + -0.006267, + -0.025716, + -0.070924, + -0.165411, + -0.116955, + 0.557368 + ], + "exponents": [ + 11720.0, + 1759.0, + 400.8, + 113.7, + 37.03, + 13.27, + 5.025, + 1.013 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 0.3023 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + 0.043018, + 0.228913, + 0.508728 + ], + "exponents": [ + 17.7, + 3.854, + 1.046 + ], + "orbital_type": "p" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 0.2753 + ], + "orbital_type": "p" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 1.185 + ], + "orbital_type": "d" + } + ] + }, + { + "atom_index": 1, + "shells": [ + { + "coefficients": [ + 0.00071, + 0.00547, + 0.027837, + 0.1048, + 0.283062, + 0.448719, + 0.270952, + 0.015458 + ], + "exponents": [ + 11720.0, + 1759.0, + 400.8, + 113.7, + 37.03, + 13.27, + 5.025, + 1.013 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + -0.00016, + -0.001263, + -0.006267, + -0.025716, + -0.070924, + -0.165411, + -0.116955, + 0.557368 + ], + "exponents": [ + 11720.0, + 1759.0, + 400.8, + 113.7, + 37.03, + 13.27, + 5.025, + 1.013 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 0.3023 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + 0.043018, + 0.228913, + 0.508728 + ], + "exponents": [ + 17.7, + 3.854, + 1.046 + ], + "orbital_type": "p" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 0.2753 + ], + "orbital_type": "p" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 1.185 + ], + "orbital_type": "d" + } + ] + }, + { + "atom_index": 2, + "shells": [ + { + "coefficients": [ + 0.00071, + 0.00547, + 0.027837, + 0.1048, + 0.283062, + 0.448719, + 0.270952, + 0.015458 + ], + "exponents": [ + 11720.0, + 1759.0, + 400.8, + 113.7, + 37.03, + 13.27, + 5.025, + 1.013 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + -0.00016, + -0.001263, + -0.006267, + -0.025716, + -0.070924, + -0.165411, + -0.116955, + 0.557368 + ], + "exponents": [ + 11720.0, + 1759.0, + 400.8, + 113.7, + 37.03, + 13.27, + 5.025, + 1.013 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 0.3023 + ], + "orbital_type": "s" + }, + { + "coefficients": [ + 0.043018, + 0.228913, + 0.508728 + ], + "exponents": [ + 17.7, + 3.854, + 1.046 + ], + "orbital_type": "p" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 0.2753 + ], + "orbital_type": "p" + }, + { + "coefficients": [ + 1.0 + ], + "exponents": [ + 1.185 + ], + "orbital_type": "d" + } + ] + } + ], + "ecp_electrons": [ + 0, + 0, + 0 + ], + "ecp_name": "cc-pvdz", + "name": "cc-pvdz", + "num_atomic_orbitals": 42, + "num_atoms": 3, + "num_shells": 18, + "structure": { + "coordinates": [ + [ + 2.3383396794226514, + -0.5018697262045597, + 0.0 + ], + [ + 0.05325650494846228, + 0.05325521871729729, + 0.0 + ], + [ + -0.5018700584633315, + 2.338340633395045, + 0.0 + ] + ], + "elements": [ + 8, + 8, + 8 + ], + "masses": [ + 15.999, + 15.999, + 15.999 + ], + "nuclear_charges": [ + 8.0, + 8.0, + 8.0 + ], + "num_atoms": 3, + "symbols": [ + "O", + "O", + "O" + ], + "units": "bohr", + "version": "0.1.0" + }, + "version": "0.1.0" + }, + "coefficients": { + "alpha": [ + [ + -0.0017613483088018916, + 0.711518310624656, + 0.7037472999089728, + -9.399455949241367e-05, + 0.001260283200973966, + -0.000668875084214835, + 0.0049818667015517435, + 0.0015708258283454223, + 2.5874245447605983e-16, + -0.000676475790391484, + -0.00706643792623164, + 4.604649764119856e-17, + 1.8397405373366005e-16, + 0.012565480068375634, + 0.005898052039455152, + 0.11191713207721744, + -1.5374355809907998e-14, + 0.06687840864819246, + 0.07651740349616816, + -0.0030466893827386476, + -0.1055084723904908, + 1.1199218234569258e-14, + -1.06477179509947e-14, + 0.03318764159041528, + 0.3346427292991582, + -0.5631349789506233, + 0.3587102802356655, + 2.1985695340438685e-15, + -0.021499647260490033, + -1.1587645359928652e-16, + 0.02094549454587577, + 0.02917115722777478, + -8.060958736978645e-16, + -1.6425192025007424e-15, + 0.06834996487394485, + 0.0688658267151557, + 0.16677540651498332, + -2.8384964822483334e-15, + 4.8897844219890855e-15, + 0.10832832612676346, + 0.15397869158292363, + 0.1912492697195178 + ], + [ + 0.00015932168684034643, + 0.0006772539235809356, + 0.00023048437592884555, + -0.1985633172167325, + -0.3113606466003104, + -0.27070335301315374, + -0.10910585651897751, + -0.16497648792835276, + 1.6278534121343272e-14, + 0.005013863525095217, + -0.010431021621965847, + -7.33057304633428e-17, + 5.563742606420083e-16, + -0.10147510747777565, + -0.13729756142329502, + 0.23665519314511257, + -4.6627852825666184e-14, + 0.21594142827170573, + 0.18581241799680553, + -0.05293983744576951, + -0.29761999129753947, + 3.062792476926799e-14, + -2.757324550331927e-14, + 0.08134789786588165, + 0.8151208534588398, + -1.2491318627415071, + 0.7738423967618882, + 4.9223901272352065e-15, + -0.024672858732974652, + -3.3680701891273864e-16, + 0.0822178765362882, + 0.027666933260897076, + -1.060293318230678e-15, + -2.3453367865827154e-15, + 0.1762152883961709, + 0.18128957242035112, + 0.36925871774552554, + -6.484538461770268e-15, + 7.737981812801435e-15, + 0.19470897587479535, + 0.23910636166119137, + 0.3116498530840739 + ], + [ + -0.001589681238226593, + -0.0032768983761073977, + -0.0021113468486820067, + -0.12583179865479674, + -0.309727298370417, + -0.36056594701840744, + -0.2406239923493408, + -0.300286015231819, + 2.7454870236320667e-14, + 0.004078341357603479, + 0.0531037886043208, + -1.0178629474267361e-15, + 8.112341528600056e-17, + -0.6155619032464832, + -0.8049432984607647, + -0.8821890986070516, + -1.1615764626128125e-13, + 0.7103285085645865, + -0.1906021313545944, + -0.12123991900690492, + 0.17934417040471085, + -2.420767969487312e-14, + 1.1555498451699517e-14, + -0.6695635785674935, + -0.5551165620389613, + 2.4262038463165325, + -1.9781727188748213, + -9.928167894319252e-15, + 0.30460225421715875, + -1.1901711152371207e-15, + 0.3495787081267972, + -0.538525175238965, + 1.3231816980767182e-14, + 2.3129469867297278e-14, + 0.07936236603108633, + 0.025759619259084265, + -0.664764142064391, + 1.1217924552695575e-14, + -4.344626320943823e-14, + -0.9028031503241328, + -1.240558722001198, + -1.4239563644443407 + ], + [ + 0.00011879210489503894, + -0.0012656106462341351, + -0.0011669747344560586, + 0.10376527771523388, + 0.09105158492923618, + -0.10939579856717607, + -0.12383275026944963, + -0.32019174781303344, + 3.0460851898998265e-14, + 0.0012982139193877288, + 0.25652213337879476, + 3.563541456198813e-16, + -6.895727376980855e-16, + 0.33867382577244537, + 0.32361693652720475, + -0.36709328259362506, + -5.1496894065507564e-14, + 0.27522749527715745, + 0.058444878083615155, + -0.5739059828216324, + -0.509384043572806, + 5.091653419998346e-14, + -2.0543379131243357e-14, + 0.07146897861298752, + -0.08826088537434426, + 0.08788162635836455, + -0.002643697174707393, + -1.4034952810459226e-15, + -0.10055090308404997, + -7.916971695035124e-17, + -0.17374677669888117, + 0.15837614763827498, + -3.519979146748753e-15, + -5.5025284998782434e-15, + -0.14594406710701505, + -0.19787161045371737, + -0.1652932162236645, + 2.90748687434069e-15, + 5.865657779998858e-15, + 0.06377906480372146, + 0.1893427092795396, + 0.17852209465434132 + ], + [ + -2.1302422111885993e-05, + 0.00040000584458483305, + 0.00026375615599977145, + -0.03348937086215691, + -0.004358880347415549, + -0.023001860107845665, + 0.2567400912495197, + -0.0314976612622805, + 1.4375703009651906e-15, + -0.4933099112024433, + 0.357413170446587, + 3.7092735626386273e-16, + -5.522352390264785e-16, + -0.15090928215272595, + 0.007332584999076089, + 0.16426015263436078, + 2.355674577997738e-14, + -0.3071713229025603, + 0.6223920876632938, + -0.23578639979622648, + -0.02367374606145615, + 6.509123611249409e-15, + 5.927185928046916e-15, + -0.5232255097751093, + -0.15614580835953817, + -0.09889188916773153, + -0.07988522034988751, + 4.529862095216803e-16, + -0.009787325853563664, + -1.0095612678369212e-16, + 0.09251851118994077, + -0.01716705733092771, + 1.6995846227301808e-15, + 1.1949459215063904e-15, + 0.13193231994120133, + 0.045269586510319336, + 0.009531790498651048, + -1.0537588571368305e-15, + -2.0847091170873643e-15, + -0.03553205580032229, + -0.043174172354372965, + -0.043349458922152535 + ], + [ + -1.8480350346778257e-16, + 1.41250331189617e-16, + -1.4148338436777513e-16, + 5.994518409959783e-16, + 5.077479867055641e-16, + -2.634195575577266e-16, + 1.4719120327691139e-15, + 2.417673775485217e-14, + 0.2610567515172919, + 9.389445316524583e-17, + 1.3979782509272695e-15, + -0.45534170942668656, + 0.3730616103322131, + 1.6503552983791064e-16, + -4.7963675652013115e-16, + 6.515092828304018e-15, + -0.471410931415238, + -8.584914925074702e-14, + -2.8341507863383373e-14, + -6.963670010177395e-15, + 8.840684485383916e-14, + 0.6852568388333309, + -0.48909025184657434, + 1.2835724607421146e-15, + -4.243341777153026e-15, + 2.2160706000008914e-15, + -4.522026024810084e-15, + -0.06476034189134464, + 1.7474771359564533e-15, + 0.04028156472703985, + -4.727863842160565e-16, + -7.480511494301423e-16, + 0.024361751055532083, + 0.03655124977078849, + 4.01567144867991e-17, + -4.8670864867640396e-17, + -6.2306745578434e-16, + -0.03165336472413785, + 0.0065037375325256545, + -2.308312685919986e-15, + -3.3749378031716085e-15, + -5.012081892918695e-16 + ], + [ + 0.0006925135359825555, + 0.0013629325219717825, + 0.0007125617404745804, + 0.016054941335634065, + 0.03907758055843969, + -0.034567529852519596, + -0.038588116673101865, + -0.16092127672708484, + 1.5676929108553442e-14, + 0.007928087634621498, + 0.1628143943544517, + 1.342823376513055e-15, + -1.3907492056563523e-15, + 0.678420931649487, + 0.811060572548184, + 0.797740548818454, + 1.4016433847686068e-13, + -0.7989485080983849, + 0.020029084110875763, + 0.8739530154395622, + 0.7860342742856201, + -7.335611718400788e-14, + 3.366454988952001e-14, + 0.24240014514988534, + 0.11717234658889061, + -1.0671381929296226, + 0.9203281304648923, + 5.314906174497753e-15, + -0.12729065484730231, + 8.716252810327436e-16, + -0.11442460877745136, + 0.1979999993212946, + -4.899607572293976e-15, + -8.856935854708068e-15, + -0.0021994504942024493, + 0.09250763922192878, + 0.45048541812721793, + -7.000374870254966e-15, + 2.224830964716879e-14, + 0.4596737239767952, + 0.7609992567912525, + 0.8193405170045229 + ], + [ + -0.000463509471573487, + -0.0003893489576391002, + -7.74480779093027e-05, + -9.186833998974275e-06, + -0.008162545072009609, + -0.01505505526278426, + 0.12574512820250514, + -0.013408958786484607, + 3.2524926791369187e-16, + -0.3497916909454886, + 0.2828849832592, + -4.0856258358509393e-17, + 1.8913922521692952e-16, + -0.30664611487388105, + -0.05368500153624419, + -0.4637995200124685, + -2.006553225500354e-14, + 0.2799801435226983, + -0.625652916458579, + 0.18748273279473754, + 0.14636917247765055, + -1.766625694167923e-14, + 1.3916548084652963e-15, + 0.850736873531966, + 0.365022470149761, + 0.22868923476260353, + -0.30517475548843165, + -8.473361627844091e-16, + 0.003859038323223647, + 4.50089906643573e-16, + -0.10898367527197976, + -0.15419968938885648, + -1.1424188256370197e-15, + 3.203324365629822e-15, + -0.20443627422890492, + 0.02602142864834334, + 0.015438404957295326, + 1.4743941445944099e-15, + -1.6370411116751388e-14, + -0.3248195121009972, + -0.17686200698523266, + -0.14111295297121318 + ], + [ + 1.1636532698013606e-16, + -1.5588341596154192e-16, + 1.0880766650822896e-16, + 3.829425894003322e-16, + 2.689915746866007e-16, + 2.757587578409129e-16, + 2.790332266104727e-16, + 1.4088960362877333e-14, + 0.15417718420310664, + -3.6051002216484057e-16, + 1.1445869737293796e-15, + -0.3680897441682019, + 0.38464932190265366, + -3.4989520604123526e-16, + 1.3208799822193764e-15, + -5.239609100628053e-15, + 0.32293175303093874, + 6.603218450101121e-14, + 2.0951207944864677e-14, + 8.426016991924388e-15, + -1.1322338968157453e-13, + -0.7921538783554328, + 0.832604838480252, + -3.466428241329757e-15, + 7.62286210620336e-15, + -3.4844184144479423e-15, + 4.246742343087103e-15, + -0.07140184670084454, + -7.686087971447673e-16, + 0.024338910721400965, + 1.325413391904308e-15, + -3.5613663596543516e-16, + -0.032689538286556144, + -0.07209477332422434, + 1.9967500064428566e-16, + 4.0817457963609506e-16, + 8.422792198167016e-16, + 0.2282306772925758, + -0.14274901944077537, + 9.006699812027153e-15, + 3.9653878344283124e-15, + 8.215633790227969e-16 + ], + [ + -2.5369137658829638e-05, + 7.43956851969767e-05, + 6.637760214646658e-05, + 0.00895134520344189, + 0.0040459634994585185, + 0.0013901073856583857, + -0.0211210064779574, + -0.0041066425855090264, + 4.767080834448631e-16, + 0.017586970822102248, + -0.0017502896243502184, + 2.6989972035650122e-17, + -9.013590235521426e-18, + -0.013654429006799975, + 0.01128480365086407, + 0.03780074052251412, + 6.0058305182444665e-15, + -0.027590895833582752, + -0.012116695725973804, + -0.02404724985623946, + 0.00932205867414363, + -4.699583650904146e-16, + 1.5271006952726165e-15, + 0.08915051086617565, + -0.0015487918355510195, + 0.02961948136019113, + -0.0865404815016752, + -9.84721574009614e-16, + -0.30899351199351444, + 1.702111571193274e-15, + 0.4599340843237217, + 0.1430412367399587, + 8.808819082813115e-15, + 6.008282029914628e-15, + 0.3046960193659428, + 0.22641584892931893, + -0.3124698751554813, + 1.2599953482819848e-15, + 3.609387911168111e-14, + 0.6536784243389747, + 0.05090683452797932, + 0.38431115969029694 + ], + [ + -1.270817712639795e-16, + 8.356048854577706e-17, + -9.850996118752719e-17, + 2.5900699545463132e-17, + -6.188449970488292e-17, + -7.690125200602923e-17, + 7.912867641673169e-17, + 5.585624077908048e-16, + 0.006718551983902079, + -4.656603857554806e-17, + 3.839682720919745e-17, + -0.004951908923828109, + -0.00047628582662786177, + 3.533974437587096e-17, + -1.3971523963490332e-16, + 2.0945212611334557e-16, + -0.003558253263976293, + -7.859960059983535e-16, + -1.8422529660206642e-16, + -3.9605588392832494e-16, + 1.2981419808070361e-15, + 0.016910409456521523, + 0.010398862137348069, + 3.1290370196262395e-16, + -1.3790044223663603e-16, + -2.3436232061197704e-16, + 2.375029703224186e-16, + 0.03747916132983966, + -8.427398305892728e-16, + -0.2686735605538165, + 5.095358064593045e-15, + 1.146482171371745e-14, + -0.7042904341557328, + 0.5959989919205798, + 4.263109287960304e-15, + -2.0087536897655615e-15, + 3.5548524505784e-15, + 0.2784802001833716, + -0.059844473326176914, + 2.0135518204233937e-15, + 9.824842934639128e-16, + -2.4553599332942103e-15 + ], + [ + -0.00016279402038978409, + -4.1993433812048596e-05, + -0.00012907356151309386, + 0.009312534928960427, + 0.007623427823470033, + -0.002758731453626304, + -0.007770047641615649, + -0.012784381783544766, + 1.2863771917162084e-15, + 0.004289097664279655, + 0.004828188307893601, + 1.1763165448547334e-16, + 1.8715473884293376e-16, + -0.0003333587859598123, + -0.007040812318519787, + 0.021173206450744067, + 6.870170532814403e-15, + -0.03691307653644089, + -0.005805505206568458, + -0.018304680005941647, + -0.011473706480055504, + 1.1034888958396258e-15, + 3.708569581538061e-16, + 0.0423550129046431, + -0.060922279558813006, + 0.09513684659277741, + 0.08917294825003427, + -1.2076236109760308e-15, + -0.10718853110247713, + 1.453885419228254e-15, + -0.3291259634750056, + -0.5899399043567829, + -5.801473429063729e-15, + 3.920089208471511e-15, + 0.47675778475722025, + -0.16905397867667005, + 0.3010461090816804, + -9.84633813595521e-16, + 1.4005570374053435e-14, + 0.19628184634539142, + 0.3449216067249908, + 0.35145704911631387 + ], + [ + -7.226857712559586e-17, + 1.776195008676595e-17, + 1.1392548901212323e-16, + -8.943828582255323e-17, + -3.144645361282775e-18, + -7.332999739597383e-17, + -1.341436674138385e-16, + -2.4215136750666306e-15, + -0.02617038672563256, + 7.880497847148807e-17, + 1.7393096197609456e-17, + 0.02196505299484301, + 0.0062261903679280695, + 1.0656050999102642e-16, + 4.545152860814954e-16, + -3.1993375190067283e-17, + 0.02512640162572376, + 4.584455765215653e-15, + 1.149307654095468e-15, + 8.074484048644328e-16, + -2.7799797686202617e-15, + -0.021008304643926216, + 0.01849240274163806, + 2.2749925345939393e-16, + -3.339843345845812e-16, + -1.8053838161993363e-15, + 9.349548527898479e-16, + -0.428431462907117, + 1.7450620777119125e-15, + 0.30400844605712857, + -6.9853615038769564e-15, + 9.754034169582665e-15, + -0.06544844107513838, + 0.37511880482093063, + 2.084348809335437e-15, + -1.0046494513768877e-15, + -2.03415893970138e-15, + -0.5620053241746479, + 0.5903724921428054, + -3.2099043292773115e-14, + -5.3532580357787044e-15, + -3.1516290904673497e-15 + ], + [ + 0.00019941074619584284, + -0.00015248221967125532, + -0.00012143877234843606, + -0.014667558998131736, + -0.013013300483294419, + 0.00981588060578977, + 0.004238730789351605, + 0.02376385002623363, + -2.1430337032580446e-15, + -0.0014798277677331032, + -0.013010859375523414, + -8.788484002947897e-17, + -3.189948175602379e-16, + 0.0037668168452689114, + 0.011391641598689125, + 0.001057945429376511, + -1.3797402895518763e-14, + 0.07703070872139414, + 0.02386187063691323, + 0.032185066465250295, + 0.012551099921569431, + -1.3623118081108904e-15, + -1.93134496370067e-15, + -0.018292147960538956, + 0.09370227874049385, + -0.04999902218526372, + -0.04863375536581155, + 3.721457252022226e-16, + -0.23978860265873322, + 2.248190762285365e-15, + -0.2147888984679387, + -0.06891856104517485, + -4.553093784338745e-15, + -4.996492875999471e-15, + 0.3441493939836149, + -0.4217317128181767, + -0.4600553561120964, + 3.3902570711759357e-15, + -5.479444221123077e-15, + 0.010247862441986071, + -0.6400227827468534, + -0.508696389294046 + ], + [ + -1.0010012371075216, + -1.4061167274331859e-05, + -0.002634855162960031, + -0.003072333750662838, + -3.6777838900388707e-09, + 0.013403477115239966, + 0.0002800171043882835, + 3.42174314914534e-08, + 1.828415108845459e-16, + -1.4254697727694438e-09, + -0.0031921586566836914, + -3.1667454342710296e-16, + 2.7109434910019556e-16, + -0.006714140088455252, + -1.3187519642568316e-07, + 0.026813743598619263, + 9.487176226790105e-16, + 5.363826564664914e-07, + -0.011000166710641062, + -1.0804944544711626e-06, + -0.15379034674636732, + 1.2422426433228657e-14, + -8.202994753713696e-15, + -1.8208843116896523e-06, + 0.3137888500002298, + -2.186734830673696e-06, + -0.746144713260582, + 3.6881817215941515e-16, + 0.17082829008531747, + -1.0264036059144819e-15, + -1.099378504572719e-05, + -0.32824319228101867, + 1.3178042936091564e-15, + 8.874400712403029e-15, + 2.2229677517506466e-07, + 0.21252920618473314, + 1.8594951989113936e-06, + 1.5020446187568966e-15, + -1.095410392776161e-14, + -0.24085642065312207, + -0.3076272177454349, + 2.578586874950061e-06 + ], + [ + -0.001073666524284968, + 2.775159955355845e-07, + 5.2067014207895814e-05, + -0.3928261010239749, + 9.78670864851219e-07, + 0.3267592703330938, + -0.016754263548965715, + -3.971134510536772e-07, + 7.629323098536348e-16, + -5.850253980317882e-07, + 0.07011891156379486, + -4.798654732431997e-16, + 4.440365902538791e-16, + 0.2586230864259066, + 1.872753092548266e-06, + 0.1439961753018607, + 3.287796254717533e-15, + 1.2577434375078693e-06, + -0.03561781282558765, + -3.270545284856942e-06, + -0.3973393350499186, + 3.247011978786863e-14, + -2.1242789876619184e-14, + -4.084482586394365e-06, + 0.7144178354189308, + -4.5220437282501325e-06, + -1.5743060202357466, + 7.618558305029821e-16, + 0.35541431175870897, + -2.126362390024033e-15, + -2.3865816706644173e-05, + -0.706476109653514, + 2.8069854152856658e-15, + 1.909092531241051e-14, + 1.3945214359220683e-07, + 0.49876597863604666, + 4.197157468747397e-06, + 2.9191204888550022e-15, + -1.6970163184254934e-14, + -0.40825570197335304, + -0.47140591990734176, + 4.448851502114274e-06 + ], + [ + 0.005422542454510997, + 6.179581362698963e-06, + 0.0011587834710626544, + -0.33522679680281886, + 1.2778947075702832e-06, + 0.4140429294363816, + -0.01148554172410358, + -1.1286558522690342e-06, + -7.383911191917963e-16, + -1.1948240501864836e-06, + 0.18883584022231414, + 4.066037299984612e-15, + -1.8574847575184068e-15, + 1.3633974812246439, + 1.3686280241303486e-05, + 1.2528727419487546, + 4.330135533611502e-15, + -3.2703123481572116e-06, + 0.24435770589861303, + 9.054051385225935e-06, + 0.9942576899960688, + -8.58197584037287e-14, + 4.24204462571216e-14, + 7.116171655132245e-06, + -0.6353592648622944, + 8.304729786062135e-06, + 3.8935997913690863, + -7.987043673837798e-16, + -0.7485112530871579, + 5.497706137465825e-15, + 4.383116882837187e-05, + 1.3473161464717132, + -7.565805596742324e-15, + -3.743776595696282e-14, + -3.2463879244564806e-06, + -0.4324861618151409, + -2.9437428082140555e-06, + -9.95175267761342e-15, + 8.892774078733299e-14, + 1.7656766928483771, + 2.5189688393158853, + -1.4746712557645627e-05 + ], + [ + -0.0012405591825331481, + 3.192826236833549e-05, + 6.216800881680539e-05, + -0.08035807369646543, + -0.21411453909915645, + -0.15056285373356795, + 0.34455640260343756, + 0.29900043377162355, + -2.920288071700987e-14, + -0.06881953368436597, + -0.23867449166725535, + -9.31914077587288e-16, + -6.571670645461179e-16, + 0.24946663471869274, + 0.3890466055802681, + -0.4765632647384737, + -9.968922372198807e-14, + 0.49211733026135845, + 0.21819015033637623, + 0.13578939019712966, + 0.2829056906383948, + -2.4387416174129088e-14, + 1.2750180344202483e-15, + -0.3020126905968096, + 0.21432691030913387, + 0.11575538244720963, + -0.04202554478662507, + -1.512667471615778e-15, + -0.0329933104822789, + -6.675954954488564e-16, + 0.16876380179674816, + -0.10065637500638636, + 3.735598413449597e-15, + 5.866836096891918e-15, + 0.10737007778830632, + 0.13600506287769687, + 0.19421228025408024, + -1.1214706473772473e-15, + 4.991898201642654e-15, + 0.046051234478171235, + 0.14615714764123386, + 0.22723133334540901 + ], + [ + -0.0012405529266072345, + -3.126450374150054e-05, + 6.250683379774272e-05, + -0.08035688483017414, + 0.21411464831739763, + -0.15056298541313606, + 0.34455541331984424, + -0.2990012073186251, + 2.544386324021274e-14, + 0.06882429994425189, + -0.23867447264020886, + -3.45348151814262e-17, + 1.1082799981697953e-15, + 0.24947208607551075, + -0.38904320428296496, + -0.4765638822525645, + 7.020792865091491e-14, + -0.4921147699637985, + 0.21819219219043381, + -0.13578112258646896, + 0.28290833955801, + -2.1805151215168236e-14, + 2.197475544836594e-14, + 0.3020121510629762, + 0.21433187505141785, + -0.11575538244006105, + -0.04202540613376373, + 7.083229202767394e-16, + -0.03299302879980649, + 6.388346218873917e-16, + -0.16877072939965365, + -0.10064705166120094, + -4.003148700659664e-15, + -4.654970776373855e-16, + -0.1073705580484886, + 0.13600484313293415, + -0.19421047689020757, + -5.811613183570061e-16, + 2.9059758295292124e-15, + 0.046050995715366076, + 0.14615199854888605, + -0.22723205623639917 + ], + [ + 7.550815613087127e-17, + 5.2339525361060326e-17, + -1.5300611039749706e-18, + 6.756925035001615e-16, + -2.373745156555218e-16, + -8.434193249870992e-16, + 3.0740190170240735e-15, + 4.629039219213014e-14, + 0.4994372599878549, + 5.313731410241504e-16, + -2.8106732358294856e-17, + 1.2460317355468467e-07, + -0.47749117640475536, + -1.3118869118643924e-16, + 7.513158216502361e-16, + 8.721148872131923e-15, + -0.7074405315510175, + -1.1201883590235598e-13, + -3.660985519979887e-14, + 4.707201014932263e-16, + -2.4482612763348014e-14, + -6.092557543795121e-06, + 0.626345343259594, + -1.074925396618705e-15, + 7.032300630316149e-15, + -1.0996882614231124e-15, + 3.43732740270206e-16, + -6.724310954594689e-07, + -1.522300644350135e-16, + 0.08821950572216442, + -1.3761501286423276e-15, + 5.716366776447079e-16, + -9.336153280657772e-08, + 0.005938367624195928, + 7.986281274309023e-17, + -7.237733767153121e-17, + 3.7509700516109532e-16, + 0.030995000156080893, + 1.5182021941761492e-07, + -2.760645509165656e-16, + 2.1403722196467027e-16, + 8.842180821794658e-16 + ], + [ + 0.0013559732853018437, + 0.0009299025348614756, + 4.244029083137401e-05, + -0.036791483680718384, + -0.05729551307716413, + -0.06168825848201445, + 0.20442803498585185, + 0.1419526006460149, + -1.3346688229899745e-14, + -0.0021050644139652404, + -0.16202016135964437, + 7.755997964035714e-17, + 3.907061178758958e-16, + 0.5122352745558014, + 0.9035344820940345, + 0.982734420398517, + 2.2851030811213454e-13, + -1.2728625971285636, + -0.08167061421440809, + 0.049798895245451606, + -0.4023187132834646, + 4.024468294715914e-14, + 2.3981561840330035e-15, + 1.0126811528888788, + -0.46904390197305934, + -1.3219387209204014, + 0.7946191815360729, + 6.4682340872018045e-15, + -0.2007942718421548, + 1.7827264354740726e-15, + -0.5510978494901214, + 0.4207765623261231, + -1.3934736080789335e-14, + -2.018894207410266e-14, + -0.3693123482008664, + -0.19345966574150078, + 0.33632343114812774, + -4.420155651135708e-15, + 2.8410638150658376e-14, + 0.6398037375178663, + 0.5044342118765871, + 1.0278499539920471 + ], + [ + 0.0013559716926445275, + -0.0009293976960642831, + 5.2356156238426254e-05, + -0.03679125900143305, + 0.05729573717552413, + -0.06168841870409751, + 0.20442779111033066, + -0.14195325347900944, + 1.0935716334628728e-14, + 0.002108606973596082, + -0.16202102675131277, + 1.3521192389788804e-15, + -6.084610497917691e-16, + 0.5122448806587175, + -0.903524885159391, + 0.982736758302364, + -2.046264669711747e-13, + 1.272859226572418, + -0.08167552425956554, + -0.04981293074079704, + -0.4023147607574893, + 2.914536865395837e-14, + -3.6909964650012043e-14, + -1.0126770990622613, + -0.4690574521653702, + 1.3219420352565974, + 0.7946077849585986, + -6.3098721525600955e-15, + -0.2007939488699487, + 1.4525911963987521e-15, + 0.5511253544307765, + 0.42074210881931334, + 1.0974992851607651e-14, + -1.3320347614512276e-15, + 0.3693106951216751, + -0.19344833162241584, + -0.33632632516572153, + 1.5641207492456734e-15, + 3.336472175288476e-14, + 0.6398055938296223, + 0.5044172209916621, + -1.0278556066458602 + ], + [ + -4.932978655726522e-16, + -3.858716378227519e-17, + 6.123100351749968e-17, + -6.041695399144834e-16, + -9.425043195842682e-17, + -3.5165198267195244e-16, + 1.914640977166994e-15, + 3.0184791532425215e-14, + 0.3175012947196149, + 5.747887747589612e-16, + -3.1420805283428995e-16, + 2.9161126060285523e-07, + -0.45464455509180113, + 2.9110216725185827e-16, + -2.699804769748e-15, + -8.97452969574768e-15, + 0.7068302142428814, + 1.0283250461628334e-13, + 3.666415078782934e-14, + -7.143906284247826e-15, + 5.059468039659547e-14, + 1.0761747902024719e-05, + -1.1789591248852218, + 4.194690256147354e-15, + -1.2402728073932051e-14, + 1.2152697296827239e-15, + -2.789376581549488e-15, + 1.066352740677053e-07, + 1.1283925680884935e-15, + 0.03925829472377486, + -1.7352869352441499e-15, + 1.6869416809188508e-15, + -2.58481577833276e-06, + 0.13023859833727228, + 2.167722824026574e-16, + -4.6611583562281654e-17, + -2.4884536469589583e-15, + -0.4193414189508609, + 2.484279946766648e-06, + -1.1530444961172796e-15, + -1.4153629052424042e-15, + 3.802717825355853e-16 + ], + [ + -4.9139972192500825e-05, + 1.2532259279523831e-06, + 0.0002349391812642442, + 0.009251829938461879, + -7.83312828880416e-09, + -0.004633293819919307, + 0.02255642428900985, + -1.1187615327688963e-07, + -7.307249494900588e-18, + -2.3565475894130017e-07, + 0.029830676238592438, + 6.937990380644147e-17, + -3.7948177260392955e-17, + 0.024687074975616462, + 1.1086946702840418e-07, + -0.09454050432555262, + -1.817558504145973e-15, + 1.545710988864917e-08, + 0.00613443603724553, + -5.049783516558052e-07, + -0.037484812192992975, + 3.2971086103830743e-15, + -1.7941581484042357e-16, + 3.5289263638528985e-08, + -0.08571045670591014, + 1.1846417237607226e-07, + 0.11228811345615068, + 2.148971982345758e-15, + 0.6086569380748019, + -2.4677677934574447e-15, + -5.82590500507937e-06, + -0.182951417682054, + 7.195879909752421e-17, + 5.1526866618719436e-15, + 5.42217326530445e-07, + 0.09406308523800401, + 9.929921354547193e-07, + -1.2623904850965279e-15, + 2.1460774781943605e-14, + 0.522157301587059, + -0.7282819362796469, + 5.833844045641243e-06 + ], + [ + 2.1752424389345974e-16, + 5.029684788106848e-17, + 1.5056354783991496e-16, + -6.901818143555954e-17, + -5.495539909486544e-17, + -7.530817129393245e-17, + 1.0486300913536804e-16, + 2.027340012490082e-15, + 0.021388142856544667, + -3.4998576939614456e-17, + -6.399827764709759e-17, + 0.03073769998788244, + 0.0031017351171815566, + -5.835656643344614e-18, + 2.98846114158256e-16, + 2.6370263257629685e-16, + -0.026625641874227387, + -4.166342503538712e-15, + -1.4846814385475334e-15, + -3.6660053850675167e-16, + -2.3562425808443625e-15, + -0.014351381344463655, + 0.018674785747310848, + 8.639683224695077e-16, + 1.5182863606969735e-16, + -1.4487072436454049e-16, + -7.79319888173439e-16, + -0.46618243745509697, + 1.299029596903125e-16, + -0.5218884376160272, + 2.103759898095543e-15, + -3.3510299093496484e-15, + -0.004814041046211707, + -0.07930951690533286, + 1.6280961228643237e-15, + -5.929643891752847e-16, + -6.104568066148214e-15, + -0.49817580354602725, + -0.5831228159617589, + 2.818900804626549e-14, + 4.587130960989423e-15, + 3.559288125141728e-15 + ], + [ + 0.0001428488476022815, + 1.9436043758412013e-06, + 0.0003643100780017828, + 0.017471740054396914, + -2.8168076362039754e-08, + 0.005902041689138691, + -0.014097693226871166, + -6.208381624593798e-10, + -1.9795894053489494e-16, + -1.434107906548935e-07, + 0.014940250475559188, + 7.083791917842222e-17, + 7.761259929568166e-17, + 0.020883717893617978, + 1.0941969442855063e-07, + -0.0749922980766121, + 6.445240793022765e-16, + -2.1950501686641712e-08, + -0.03785700620950398, + -8.734572358556348e-07, + -0.06829986604067217, + 6.476662697268637e-15, + -1.6283074560511371e-15, + 3.0772918165903135e-07, + -0.1347473303583918, + 5.843660360841642e-07, + 0.15702177215140115, + -1.3410985851144486e-15, + -0.3403731399455043, + 1.2972151036925361e-15, + -1.1494400704410922e-05, + -0.29633659811132707, + -1.615721246409904e-15, + 5.1871747494326175e-15, + -2.3202762266790163e-06, + 0.6168826841232165, + 3.46058918279828e-06, + 1.2150919913506634e-15, + -2.7383966979682646e-14, + -0.46821471299791445, + -0.6359807074320812, + 4.104162972750396e-06 + ], + [ + -8.31616302355406e-17, + 1.5390985820428067e-16, + -8.029479617064937e-17, + 5.65497348634005e-17, + 2.2467173678619488e-17, + -6.654482314491992e-17, + 1.6868291630511466e-16, + 2.144174518476808e-15, + 0.021388331658808026, + 1.142629404874943e-16, + 1.5663726775540034e-16, + -0.03073768980583212, + 0.00310179026247711, + 7.675970593775375e-17, + 4.579555170867327e-16, + 4.2387443241620065e-16, + -0.02662599700566266, + -4.497521198588121e-15, + -1.6509121171145228e-15, + 1.6380923132653128e-16, + 6.246024714100529e-16, + 0.014351238047080328, + 0.018674867070117777, + 9.85192009133767e-16, + 1.9652093408234633e-16, + 2.427627793232047e-15, + 5.18079899597404e-16, + 0.4661836112042665, + -4.2864112100471076e-15, + -0.5218867104693762, + 1.990730870216727e-16, + -7.098139819591328e-16, + 0.00481776677788859, + -0.0793089766590969, + 7.577646201689517e-16, + -1.05164554990519e-15, + -2.14778068438821e-15, + -0.49817067647520435, + 0.583128384335261, + -3.114073256964781e-14, + -5.355931475673746e-15, + -1.236105130627625e-15 + ], + [ + -1.258212042779213e-09, + -0.0003476842777311292, + 1.852122554267521e-06, + -1.1488269542528357e-07, + -0.03507609137664523, + -1.763644820865269e-08, + 1.1399252056516555e-07, + 0.02158794860148395, + -1.869055864127683e-15, + -0.029710130493980757, + -1.929384017379902e-07, + -6.322770343923514e-17, + -1.9606563752652493e-16, + 2.3001979828800917e-07, + -0.03297849064749296, + 3.918052259176637e-07, + 3.243558202731441e-15, + -0.02616486300903077, + 3.0651101548176644e-07, + 0.1260946536842911, + -1.3505188482803279e-06, + 1.838505007665779e-16, + -2.167097189346346e-15, + -0.09586832737159776, + -6.037601511145374e-07, + -0.12401770920517666, + 8.234648582124874e-07, + -6.12044408126081e-16, + 4.3489906716544065e-08, + -1.678742502355663e-15, + -0.4167292200415101, + 1.1885870168070753e-05, + -1.0043225081069346e-14, + -3.6821696713595675e-15, + -0.27962259180562377, + -6.92740899073739e-07, + -0.4351843272595588, + 6.075533741215819e-15, + 5.656753018663876e-15, + -9.307983166908034e-08, + 8.211849831382716e-06, + 1.0337225104750014 + ], + [ + -0.0017613300505372252, + -0.7039736902274834, + 0.7112943205776673, + -9.398298676863867e-05, + -0.0012603242328684708, + -0.0006688484801919751, + 0.004981847646505617, + -0.0015708111746075254, + -8.017393986435142e-17, + 0.0006765724056206014, + -0.007066406468787232, + 5.051739225088102e-16, + 7.147785599797437e-17, + 0.01256555217393601, + -0.005897784564322523, + 0.11191680173717093, + 8.106921876791239e-15, + -0.06687742323188318, + 0.0765178001208526, + 0.0030450520715755614, + -0.10550866456335226, + 8.777129317747287e-15, + -7.45794380354549e-15, + -0.03319048947718257, + 0.3346417862946701, + 0.5631376955590275, + 0.3587062263971887, + -1.9163775248931924e-15, + -0.021499153190363295, + 1.1532468664089402e-15, + -0.02094361080996321, + 0.029171806206018353, + -8.565931156225149e-16, + 4.167620957623335e-16, + -0.06835062933105666, + 0.06886646758261143, + -0.16677440399611618, + 6.930759715500321e-16, + 6.151704134479237e-15, + 0.10832874820663324, + 0.15397612051762954, + -0.19125184286022504 + ], + [ + 0.0001593217709164905, + -0.0006747553872258788, + 0.00023769096887512508, + -0.19856170361980904, + 0.311361515925907, + -0.2707031847860684, + -0.10910523548817863, + 0.16497737909353788, + -1.5204554738056677e-14, + -0.0050140495157909816, + -0.010430691644906108, + 9.8617158714887e-16, + 4.270795952617853e-17, + -0.1014766390289783, + 0.13729603293912887, + 0.2366542953552118, + 2.7724634382800118e-14, + -0.21593922586146483, + 0.18581388099238663, + 0.0529349494515786, + -0.2976215243986164, + 2.5241414156809218e-14, + -1.9852989964824295e-14, + -0.08135491813088488, + 0.8151188642566387, + 1.249137935968453, + 0.7738334745615227, + -4.137369421713338e-15, + -0.024671688227025104, + 2.4212849067939743e-15, + -0.08221630174079354, + 0.027670402570450437, + -2.6556892358849585e-15, + 1.8870870495248756e-15, + -0.17621681786467513, + 0.18129062162534024, + -0.3692561999846878, + 1.9190020035777542e-15, + 1.13980228989775e-14, + 0.19470984672436598, + 0.23910253489418548, + -0.31165435138767184 + ], + [ + -0.0015896791394839176, + 0.0032541967805489, + -0.002146170176443986, + -0.12583065669092253, + 0.3097280331221159, + -0.3605655553736356, + -0.2406226796125805, + 0.3002873533106203, + -2.5012688839749276e-14, + -0.00407998089672773, + 0.053104544602295994, + -2.9950598304183213e-15, + 4.1141499610879074e-16, + -0.6155690213224254, + 0.8049304065404382, + -0.882187800625097, + 1.215698629222247e-13, + -0.7103274244204719, + -0.19059895684270486, + 0.12124437842551516, + 0.17934033079892978, + -1.0722701928479029e-14, + 2.702389782825458e-14, + 0.6695656170654409, + -0.5551065293748615, + -2.4262136481857812, + -1.9781506261994595, + 9.630084792624026e-15, + 0.304599974092615, + -4.991164705822913e-15, + -0.3496132432462423, + -0.5385003009817542, + -5.004483914614567e-15, + 4.902566723803469e-15, + -0.0793584126475074, + 0.025749455725204057, + 0.6647640764611239, + -8.079919194652267e-16, + -4.8772971474900233e-14, + -0.9028036202143551, + -1.24053396886872, + 1.423970854736291 + ], + [ + -2.1302774418900985e-05, + -0.0003971703599785441, + 0.00026800634201995727, + -0.03348923539414831, + 0.0043591580205813085, + -0.0230017080057999, + 0.25673848846013386, + 0.03149573189863999, + -4.6724734535603716e-15, + 0.4933053220279898, + 0.3574203671885523, + 9.09828247095257e-16, + 9.6092234818424e-16, + -0.15090936059797616, + -0.007334298888454495, + 0.16426080702326465, + -8.403896948094573e-14, + 0.3071745299874465, + 0.6223888108789958, + 0.2357842560421918, + -0.02368051610147158, + 2.8689432988186593e-15, + -4.363279703678358e-15, + 0.5232292110330152, + -0.15614284277523915, + 0.09889137060334598, + -0.07988555297001854, + 2.1063143481847968e-16, + -0.00978630273172361, + 7.374641321923923e-16, + -0.09251980940178464, + -0.017162060020486373, + -1.9947811514246456e-15, + -2.7362210660381774e-16, + -0.1319325975678938, + 0.0452679184115815, + -0.009531243156416808, + 7.93606378920358e-16, + -2.2964872962877536e-15, + -0.035531823986098464, + -0.04317345137402883, + 0.04334963669813458 + ], + [ + 0.00011879352608077455, + 0.0012530919370605737, + -0.0011804004068441964, + 0.10376441173300982, + -0.09105189500507013, + -0.10939513892070361, + -0.12383098009361516, + 0.32019103586597136, + -2.9582825975857715e-14, + -0.0013022756581949938, + 0.2565230937526246, + 9.133280487401617e-16, + 2.697929238486124e-16, + 0.3386787088090443, + -0.32361278830264295, + -0.3670918572329757, + 2.931036371882568e-14, + -0.27522623007514957, + 0.058447134435822966, + 0.5738949787331941, + -0.5093974864886626, + 4.438337414083397e-14, + -2.6073330202405912e-14, + -0.07146968210957057, + -0.08826338763038255, + -0.08788227419365917, + -0.0026431002911249894, + 4.938956140446418e-16, + -0.10055125925046259, + 5.155904499309114e-16, + 0.17375779596770438, + 0.15836636379518715, + 4.445705777536366e-15, + -3.397591938350395e-16, + 0.1459447318632273, + -0.1978706059183265, + 0.1652906721149198, + -2.251278140093081e-15, + 2.3594307259089997e-15, + 0.06377878688893793, + 0.1893386208194984, + -0.17852320218695433 + ], + [ + -1.4309566617794539e-16, + -5.2240027241748233e-17, + -5.366488341965129e-18, + 1.8229732623175092e-16, + -2.108601910021458e-16, + -3.000178277328976e-16, + 8.748015708749215e-16, + 2.4565077608093327e-14, + 0.2610555058774571, + -7.357794379994173e-16, + -6.817950429749327e-16, + 0.4553432165137196, + 0.3730606237681433, + 7.059495403916955e-17, + 9.537893497896494e-16, + 3.3947109364688394e-15, + -0.4714092201251421, + -8.760264753351613e-14, + -2.0801999445627096e-14, + -1.4773032001525013e-14, + -3.529203938362898e-14, + -0.6852502003098183, + -0.4891013148244063, + 2.3277002625123767e-15, + -4.961239615007981e-15, + -1.4425026532856373e-16, + -5.967944324567362e-16, + 0.06475946353947955, + -3.9910486362367187e-17, + 0.04028119991191867, + -5.321090633512867e-16, + 7.900843068395309e-16, + -0.02436319440380709, + 0.03655039702636404, + -3.597973447746488e-17, + 4.880790729097958e-16, + -8.524372645440926e-16, + -0.03165369473305965, + -0.006503798265703627, + 1.885920606665614e-16, + -1.4924679300573276e-16, + -1.0611587489666756e-16 + ], + [ + -0.00046350996362259667, + 0.0003885018456417203, + -8.159541825817855e-05, + -9.14057740344396e-06, + 0.008162599124011832, + -0.01505500738868116, + 0.12574424464946918, + 0.013407918736240537, + -1.7337494237266964e-15, + 0.3497880070524966, + 0.2828900593441216, + 5.7188365573056e-16, + -6.054745908615651e-16, + -0.30664645916641126, + 0.05368063523913842, + -0.4637992976244009, + 7.670388878568893e-14, + -0.27998311552236155, + -0.6256482027368823, + -0.1874758885217344, + 0.14637553145442075, + -1.419859801191204e-14, + 6.686266845444978e-15, + -0.8507416869259571, + 0.36501640717401546, + -0.22869121306322182, + -0.30517305969622777, + -2.870157473950213e-16, + 0.00385794573589821, + -1.86161195430529e-15, + 0.10897332214071645, + -0.15420549795525237, + 2.6030974633703204e-15, + 4.803122880621867e-15, + 0.20443688328511134, + 0.026024320631515094, + -0.015437585917301242, + 4.958432965169636e-16, + -1.4271276077658225e-14, + -0.32481890304458455, + -0.17686013475998155, + 0.14111544848111712 + ], + [ + 0.0006925141255544204, + -0.0013552560245302344, + 0.0007270548487615724, + 0.01605473376738698, + -0.039077630320965055, + -0.03456730954087202, + -0.0385872484115535, + 0.1609208048569433, + -1.6470307511335543e-14, + -0.007930514214979542, + 0.16281488537282898, + 2.0336769806409827e-15, + -3.601423258571867e-16, + 0.6784299591451881, + -0.811048716772857, + 0.7977388976037361, + -1.1386588208137066e-13, + 0.798947382035201, + 0.020023567587810542, + -0.8739359954156816, + 0.7860550593396991, + -7.098191700530822e-14, + 3.3544565797600985e-14, + -0.2423979968761302, + 0.11716997025686536, + 1.0671430577996348, + 0.920318212694082, + -5.037957127381846e-15, + -0.12728962133089805, + 1.555773677688173e-15, + 0.11443710770481412, + 0.19799111997630087, + 6.449095090802989e-16, + -4.6950804854848115e-15, + 0.0021971626684818783, + 0.09251292626446021, + -0.45048405367278255, + 7.536831655314065e-16, + 2.6019628748097452e-14, + 0.45967445538372714, + 0.7609855470593834, + -0.8193502170345943 + ], + [ + 9.941902557767123e-19, + 3.9524331179906395e-17, + -4.013368368931335e-18, + 1.1609496276492806e-16, + -1.838894341763903e-16, + -6.335561754843437e-17, + 7.193592823879593e-16, + 1.3634064274756099e-14, + 0.1541764160964085, + -7.402164047715438e-16, + -1.8849187160488729e-16, + 0.36809088937681844, + 0.38464811389448744, + -4.052407285630491e-16, + 2.5573287919020296e-16, + -1.1173935659694867e-15, + 0.32292963219866677, + 6.819808298624103e-14, + 1.2453401034743436e-14, + 1.7845665429410595e-14, + 3.0251925732049343e-14, + 0.7921410947337875, + 0.8326170925760483, + -4.374593187199196e-15, + 8.925744519623895e-15, + 1.2128871022414656e-15, + 2.5246631298329e-15, + 0.07140221889563941, + -5.084461983430804e-16, + 0.02433954162481145, + 7.300547892713681e-16, + -2.761330350896548e-16, + 0.03269227221591637, + -0.072093477112905, + -3.5874932673186177e-16, + -4.306813479123662e-16, + 2.137318266269459e-15, + 0.22823163910454128, + 0.1427463422107145, + -6.188596483025551e-15, + -9.481149016852375e-17, + -1.3310820671260475e-15 + ], + [ + -2.536791514165615e-05, + -7.368484754010199e-05, + 6.716801033864665e-05, + 0.00895130898742157, + -0.004046033979091001, + 0.0013900974463644483, + -0.02112087593129741, + 0.004106788201742911, + -5.061768783329361e-16, + -0.017587004326387483, + -0.0017505281535641923, + -4.662696804922696e-17, + 3.3157019155371244e-17, + -0.013654234082026286, + -0.011284963861219301, + 0.03780073564003595, + -4.106738883612274e-15, + 0.027590776215515964, + -0.012116536999196346, + 0.024047506355676644, + 0.009321706323079966, + -5.688376173091981e-16, + -8.916878172122175e-16, + -0.08915044275324883, + -0.0015498095626796055, + -0.02962000582878333, + -0.08654003570732069, + -1.400820347524685e-15, + -0.30899239946043844, + -6.859316519965465e-16, + -0.4599270914110555, + 0.14306982140900384, + -1.0419282671209046e-14, + -9.719160397836612e-15, + -0.30469993610010165, + 0.22640696575645444, + 0.3124723212482826, + -3.948702801982127e-15, + 2.935248745243979e-14, + 0.6536776133688468, + 0.050902422774331955, + -0.38431112364017084 + ], + [ + -5.887023056144293e-18, + -9.197477353987915e-17, + 1.9156727911589254e-16, + -1.2986286351701265e-16, + -8.681171385924841e-17, + -1.1640376421991494e-18, + -1.0471343086374296e-16, + -2.35981957581066e-15, + -0.026170225653117465, + 5.943576761660668e-17, + 3.0081166207421423e-17, + -0.02196507412962472, + 0.006226199355940923, + 8.636686254927274e-18, + 8.168617534943777e-17, + -4.942145693620185e-16, + 0.025126038437548966, + 5.9640824436240554e-15, + 5.206793311754575e-16, + -4.8808857014645754e-17, + 8.281298065950059e-16, + 0.02100801856152951, + 0.01849284373650161, + -1.0572341365605352e-15, + -4.292535202826627e-16, + 1.8363372150960536e-15, + -9.729563974303852e-16, + 0.4284300906913619, + -4.830393855232485e-16, + 0.30400956679385815, + -9.13373061649192e-15, + 6.463303314248677e-15, + 0.06543408002086683, + 0.37512236450482184, + 1.4446105371679421e-15, + 4.975664899759549e-16, + -4.750072127325573e-15, + -0.5620111022828179, + -0.5903661724870815, + 2.8060619567726474e-14, + 4.898174389848054e-15, + 4.9248423714119294e-15 + ], + [ + -0.00016279249340948624, + 4.061266495791603e-05, + -0.00012951174303520822, + 0.009312477658289459, + -0.007623469438667967, + -0.002758711836439576, + -0.0077699786507163665, + 0.012784397040472528, + -1.180977763205326e-15, + -0.004289207979364994, + 0.004828167781991947, + 1.7916372162398945e-16, + -4.252282720005893e-17, + -0.00033343081619599096, + 0.0070407864001065905, + 0.021173185488919576, + -6.206560211629025e-15, + 0.03691280246467279, + -0.005805594286721782, + 0.01830423670489943, + -0.011474100087736253, + 1.0287660867640863e-15, + -8.733346564627528e-16, + -0.04235451649188022, + -0.060922728035432507, + -0.09513638194129317, + 0.08917337934428607, + 1.2671344145774996e-16, + -0.10718793802562869, + -6.294815442539691e-17, + 0.3290900863609058, + -0.5899640731421419, + 8.400889222231962e-15, + 2.1931206010522772e-14, + -0.47675330512334513, + -0.16905318794989735, + -0.3010456721187955, + 3.8738272911470583e-16, + 1.3645733716227054e-14, + 0.19628210327386886, + 0.3449160183381891, + -0.35146170675284094 + ], + [ + -1.0372416947701155e-16, + 2.1540387776205482e-17, + -4.677855207288378e-17, + -2.7975603887133102e-18, + -3.677958873669241e-17, + -1.1740023438840006e-16, + 2.1018893665934972e-17, + 6.02133592655218e-16, + 0.006718527095527946, + -1.5015341087039998e-17, + 3.6404112862690624e-17, + 0.0049519320418373175, + -0.0004762870104316398, + 2.843856645510013e-17, + -2.6652439424657585e-16, + 2.8689176484437154e-16, + -0.0035581622472791777, + -6.503166996488833e-16, + 1.7268970566068356e-16, + -5.039927298122869e-16, + -1.5181498464797696e-15, + -0.016910542174437996, + 0.010398523311268561, + 5.260383858423728e-16, + 4.946067784365688e-17, + -1.5704721638779545e-15, + 2.167455325738879e-15, + -0.03747918474317207, + -1.6689294000632852e-15, + -0.26867236575213177, + -2.5715954874157724e-14, + 1.4198185070048624e-14, + 0.7042697017754728, + 0.5960241584192306, + 4.5516784663927806e-15, + 1.0401229438979508e-15, + 2.4950288471129013e-15, + 0.278480290703868, + 0.0598424296121514, + -4.0397850821947335e-15, + -1.1245396117706782e-15, + -2.327635838878825e-15 + ], + [ + -0.000199408835729763, + -0.0001511802900839243, + 0.0001230597142560452, + 0.014667454698213456, + -0.01301335452150573, + -0.009815829780834154, + -0.004238633050417151, + 0.02376381235675366, + -2.2038939010631763e-15, + -0.0014800310264468776, + 0.013010922913857843, + 4.1869931377365855e-17, + -4.1251749764666887e-16, + -0.0037669387273488153, + 0.011391536794820226, + -0.0010579372303174575, + -1.241349538406943e-14, + 0.07703009871135336, + -0.023862275066472382, + 0.03218456585682706, + -0.012551978277172233, + 1.5135176991456084e-15, + -1.5740904032765052e-15, + -0.018291448810810712, + -0.09370277279435005, + -0.04999921100720285, + 0.048634073569554424, + 1.6927595621620924e-15, + 0.23978843466263194, + -5.277010638132867e-16, + -0.21478636234351697, + 0.06893241986380544, + -5.5680485514386706e-15, + -7.377347765510867e-15, + 0.34414605722690944, + 0.42173804513701674, + -0.46005166393851354, + 2.133614791380758e-15, + 3.419828578416071e-15, + -0.010246627554700865, + 0.6400145339250181, + -0.5087048653287369 + ] + ], + "beta": [ + [ + -0.0017613483088018916, + 0.711518310624656, + 0.7037472999089728, + -9.399455949241367e-05, + 0.001260283200973966, + -0.000668875084214835, + 0.0049818667015517435, + 0.0015708258283454223, + 2.5874245447605983e-16, + -0.000676475790391484, + -0.00706643792623164, + 4.604649764119856e-17, + 1.8397405373366005e-16, + 0.012565480068375634, + 0.005898052039455152, + 0.11191713207721744, + -1.5374355809907998e-14, + 0.06687840864819246, + 0.07651740349616816, + -0.0030466893827386476, + -0.1055084723904908, + 1.1199218234569258e-14, + -1.06477179509947e-14, + 0.03318764159041528, + 0.3346427292991582, + -0.5631349789506233, + 0.3587102802356655, + 2.1985695340438685e-15, + -0.021499647260490033, + -1.1587645359928652e-16, + 0.02094549454587577, + 0.02917115722777478, + -8.060958736978645e-16, + -1.6425192025007424e-15, + 0.06834996487394485, + 0.0688658267151557, + 0.16677540651498332, + -2.8384964822483334e-15, + 4.8897844219890855e-15, + 0.10832832612676346, + 0.15397869158292363, + 0.1912492697195178 + ], + [ + 0.00015932168684034643, + 0.0006772539235809356, + 0.00023048437592884555, + -0.1985633172167325, + -0.3113606466003104, + -0.27070335301315374, + -0.10910585651897751, + -0.16497648792835276, + 1.6278534121343272e-14, + 0.005013863525095217, + -0.010431021621965847, + -7.33057304633428e-17, + 5.563742606420083e-16, + -0.10147510747777565, + -0.13729756142329502, + 0.23665519314511257, + -4.6627852825666184e-14, + 0.21594142827170573, + 0.18581241799680553, + -0.05293983744576951, + -0.29761999129753947, + 3.062792476926799e-14, + -2.757324550331927e-14, + 0.08134789786588165, + 0.8151208534588398, + -1.2491318627415071, + 0.7738423967618882, + 4.9223901272352065e-15, + -0.024672858732974652, + -3.3680701891273864e-16, + 0.0822178765362882, + 0.027666933260897076, + -1.060293318230678e-15, + -2.3453367865827154e-15, + 0.1762152883961709, + 0.18128957242035112, + 0.36925871774552554, + -6.484538461770268e-15, + 7.737981812801435e-15, + 0.19470897587479535, + 0.23910636166119137, + 0.3116498530840739 + ], + [ + -0.001589681238226593, + -0.0032768983761073977, + -0.0021113468486820067, + -0.12583179865479674, + -0.309727298370417, + -0.36056594701840744, + -0.2406239923493408, + -0.300286015231819, + 2.7454870236320667e-14, + 0.004078341357603479, + 0.0531037886043208, + -1.0178629474267361e-15, + 8.112341528600056e-17, + -0.6155619032464832, + -0.8049432984607647, + -0.8821890986070516, + -1.1615764626128125e-13, + 0.7103285085645865, + -0.1906021313545944, + -0.12123991900690492, + 0.17934417040471085, + -2.420767969487312e-14, + 1.1555498451699517e-14, + -0.6695635785674935, + -0.5551165620389613, + 2.4262038463165325, + -1.9781727188748213, + -9.928167894319252e-15, + 0.30460225421715875, + -1.1901711152371207e-15, + 0.3495787081267972, + -0.538525175238965, + 1.3231816980767182e-14, + 2.3129469867297278e-14, + 0.07936236603108633, + 0.025759619259084265, + -0.664764142064391, + 1.1217924552695575e-14, + -4.344626320943823e-14, + -0.9028031503241328, + -1.240558722001198, + -1.4239563644443407 + ], + [ + 0.00011879210489503894, + -0.0012656106462341351, + -0.0011669747344560586, + 0.10376527771523388, + 0.09105158492923618, + -0.10939579856717607, + -0.12383275026944963, + -0.32019174781303344, + 3.0460851898998265e-14, + 0.0012982139193877288, + 0.25652213337879476, + 3.563541456198813e-16, + -6.895727376980855e-16, + 0.33867382577244537, + 0.32361693652720475, + -0.36709328259362506, + -5.1496894065507564e-14, + 0.27522749527715745, + 0.058444878083615155, + -0.5739059828216324, + -0.509384043572806, + 5.091653419998346e-14, + -2.0543379131243357e-14, + 0.07146897861298752, + -0.08826088537434426, + 0.08788162635836455, + -0.002643697174707393, + -1.4034952810459226e-15, + -0.10055090308404997, + -7.916971695035124e-17, + -0.17374677669888117, + 0.15837614763827498, + -3.519979146748753e-15, + -5.5025284998782434e-15, + -0.14594406710701505, + -0.19787161045371737, + -0.1652932162236645, + 2.90748687434069e-15, + 5.865657779998858e-15, + 0.06377906480372146, + 0.1893427092795396, + 0.17852209465434132 + ], + [ + -2.1302422111885993e-05, + 0.00040000584458483305, + 0.00026375615599977145, + -0.03348937086215691, + -0.004358880347415549, + -0.023001860107845665, + 0.2567400912495197, + -0.0314976612622805, + 1.4375703009651906e-15, + -0.4933099112024433, + 0.357413170446587, + 3.7092735626386273e-16, + -5.522352390264785e-16, + -0.15090928215272595, + 0.007332584999076089, + 0.16426015263436078, + 2.355674577997738e-14, + -0.3071713229025603, + 0.6223920876632938, + -0.23578639979622648, + -0.02367374606145615, + 6.509123611249409e-15, + 5.927185928046916e-15, + -0.5232255097751093, + -0.15614580835953817, + -0.09889188916773153, + -0.07988522034988751, + 4.529862095216803e-16, + -0.009787325853563664, + -1.0095612678369212e-16, + 0.09251851118994077, + -0.01716705733092771, + 1.6995846227301808e-15, + 1.1949459215063904e-15, + 0.13193231994120133, + 0.045269586510319336, + 0.009531790498651048, + -1.0537588571368305e-15, + -2.0847091170873643e-15, + -0.03553205580032229, + -0.043174172354372965, + -0.043349458922152535 + ], + [ + -1.8480350346778257e-16, + 1.41250331189617e-16, + -1.4148338436777513e-16, + 5.994518409959783e-16, + 5.077479867055641e-16, + -2.634195575577266e-16, + 1.4719120327691139e-15, + 2.417673775485217e-14, + 0.2610567515172919, + 9.389445316524583e-17, + 1.3979782509272695e-15, + -0.45534170942668656, + 0.3730616103322131, + 1.6503552983791064e-16, + -4.7963675652013115e-16, + 6.515092828304018e-15, + -0.471410931415238, + -8.584914925074702e-14, + -2.8341507863383373e-14, + -6.963670010177395e-15, + 8.840684485383916e-14, + 0.6852568388333309, + -0.48909025184657434, + 1.2835724607421146e-15, + -4.243341777153026e-15, + 2.2160706000008914e-15, + -4.522026024810084e-15, + -0.06476034189134464, + 1.7474771359564533e-15, + 0.04028156472703985, + -4.727863842160565e-16, + -7.480511494301423e-16, + 0.024361751055532083, + 0.03655124977078849, + 4.01567144867991e-17, + -4.8670864867640396e-17, + -6.2306745578434e-16, + -0.03165336472413785, + 0.0065037375325256545, + -2.308312685919986e-15, + -3.3749378031716085e-15, + -5.012081892918695e-16 + ], + [ + 0.0006925135359825555, + 0.0013629325219717825, + 0.0007125617404745804, + 0.016054941335634065, + 0.03907758055843969, + -0.034567529852519596, + -0.038588116673101865, + -0.16092127672708484, + 1.5676929108553442e-14, + 0.007928087634621498, + 0.1628143943544517, + 1.342823376513055e-15, + -1.3907492056563523e-15, + 0.678420931649487, + 0.811060572548184, + 0.797740548818454, + 1.4016433847686068e-13, + -0.7989485080983849, + 0.020029084110875763, + 0.8739530154395622, + 0.7860342742856201, + -7.335611718400788e-14, + 3.366454988952001e-14, + 0.24240014514988534, + 0.11717234658889061, + -1.0671381929296226, + 0.9203281304648923, + 5.314906174497753e-15, + -0.12729065484730231, + 8.716252810327436e-16, + -0.11442460877745136, + 0.1979999993212946, + -4.899607572293976e-15, + -8.856935854708068e-15, + -0.0021994504942024493, + 0.09250763922192878, + 0.45048541812721793, + -7.000374870254966e-15, + 2.224830964716879e-14, + 0.4596737239767952, + 0.7609992567912525, + 0.8193405170045229 + ], + [ + -0.000463509471573487, + -0.0003893489576391002, + -7.74480779093027e-05, + -9.186833998974275e-06, + -0.008162545072009609, + -0.01505505526278426, + 0.12574512820250514, + -0.013408958786484607, + 3.2524926791369187e-16, + -0.3497916909454886, + 0.2828849832592, + -4.0856258358509393e-17, + 1.8913922521692952e-16, + -0.30664611487388105, + -0.05368500153624419, + -0.4637995200124685, + -2.006553225500354e-14, + 0.2799801435226983, + -0.625652916458579, + 0.18748273279473754, + 0.14636917247765055, + -1.766625694167923e-14, + 1.3916548084652963e-15, + 0.850736873531966, + 0.365022470149761, + 0.22868923476260353, + -0.30517475548843165, + -8.473361627844091e-16, + 0.003859038323223647, + 4.50089906643573e-16, + -0.10898367527197976, + -0.15419968938885648, + -1.1424188256370197e-15, + 3.203324365629822e-15, + -0.20443627422890492, + 0.02602142864834334, + 0.015438404957295326, + 1.4743941445944099e-15, + -1.6370411116751388e-14, + -0.3248195121009972, + -0.17686200698523266, + -0.14111295297121318 + ], + [ + 1.1636532698013606e-16, + -1.5588341596154192e-16, + 1.0880766650822896e-16, + 3.829425894003322e-16, + 2.689915746866007e-16, + 2.757587578409129e-16, + 2.790332266104727e-16, + 1.4088960362877333e-14, + 0.15417718420310664, + -3.6051002216484057e-16, + 1.1445869737293796e-15, + -0.3680897441682019, + 0.38464932190265366, + -3.4989520604123526e-16, + 1.3208799822193764e-15, + -5.239609100628053e-15, + 0.32293175303093874, + 6.603218450101121e-14, + 2.0951207944864677e-14, + 8.426016991924388e-15, + -1.1322338968157453e-13, + -0.7921538783554328, + 0.832604838480252, + -3.466428241329757e-15, + 7.62286210620336e-15, + -3.4844184144479423e-15, + 4.246742343087103e-15, + -0.07140184670084454, + -7.686087971447673e-16, + 0.024338910721400965, + 1.325413391904308e-15, + -3.5613663596543516e-16, + -0.032689538286556144, + -0.07209477332422434, + 1.9967500064428566e-16, + 4.0817457963609506e-16, + 8.422792198167016e-16, + 0.2282306772925758, + -0.14274901944077537, + 9.006699812027153e-15, + 3.9653878344283124e-15, + 8.215633790227969e-16 + ], + [ + -2.5369137658829638e-05, + 7.43956851969767e-05, + 6.637760214646658e-05, + 0.00895134520344189, + 0.0040459634994585185, + 0.0013901073856583857, + -0.0211210064779574, + -0.0041066425855090264, + 4.767080834448631e-16, + 0.017586970822102248, + -0.0017502896243502184, + 2.6989972035650122e-17, + -9.013590235521426e-18, + -0.013654429006799975, + 0.01128480365086407, + 0.03780074052251412, + 6.0058305182444665e-15, + -0.027590895833582752, + -0.012116695725973804, + -0.02404724985623946, + 0.00932205867414363, + -4.699583650904146e-16, + 1.5271006952726165e-15, + 0.08915051086617565, + -0.0015487918355510195, + 0.02961948136019113, + -0.0865404815016752, + -9.84721574009614e-16, + -0.30899351199351444, + 1.702111571193274e-15, + 0.4599340843237217, + 0.1430412367399587, + 8.808819082813115e-15, + 6.008282029914628e-15, + 0.3046960193659428, + 0.22641584892931893, + -0.3124698751554813, + 1.2599953482819848e-15, + 3.609387911168111e-14, + 0.6536784243389747, + 0.05090683452797932, + 0.38431115969029694 + ], + [ + -1.270817712639795e-16, + 8.356048854577706e-17, + -9.850996118752719e-17, + 2.5900699545463132e-17, + -6.188449970488292e-17, + -7.690125200602923e-17, + 7.912867641673169e-17, + 5.585624077908048e-16, + 0.006718551983902079, + -4.656603857554806e-17, + 3.839682720919745e-17, + -0.004951908923828109, + -0.00047628582662786177, + 3.533974437587096e-17, + -1.3971523963490332e-16, + 2.0945212611334557e-16, + -0.003558253263976293, + -7.859960059983535e-16, + -1.8422529660206642e-16, + -3.9605588392832494e-16, + 1.2981419808070361e-15, + 0.016910409456521523, + 0.010398862137348069, + 3.1290370196262395e-16, + -1.3790044223663603e-16, + -2.3436232061197704e-16, + 2.375029703224186e-16, + 0.03747916132983966, + -8.427398305892728e-16, + -0.2686735605538165, + 5.095358064593045e-15, + 1.146482171371745e-14, + -0.7042904341557328, + 0.5959989919205798, + 4.263109287960304e-15, + -2.0087536897655615e-15, + 3.5548524505784e-15, + 0.2784802001833716, + -0.059844473326176914, + 2.0135518204233937e-15, + 9.824842934639128e-16, + -2.4553599332942103e-15 + ], + [ + -0.00016279402038978409, + -4.1993433812048596e-05, + -0.00012907356151309386, + 0.009312534928960427, + 0.007623427823470033, + -0.002758731453626304, + -0.007770047641615649, + -0.012784381783544766, + 1.2863771917162084e-15, + 0.004289097664279655, + 0.004828188307893601, + 1.1763165448547334e-16, + 1.8715473884293376e-16, + -0.0003333587859598123, + -0.007040812318519787, + 0.021173206450744067, + 6.870170532814403e-15, + -0.03691307653644089, + -0.005805505206568458, + -0.018304680005941647, + -0.011473706480055504, + 1.1034888958396258e-15, + 3.708569581538061e-16, + 0.0423550129046431, + -0.060922279558813006, + 0.09513684659277741, + 0.08917294825003427, + -1.2076236109760308e-15, + -0.10718853110247713, + 1.453885419228254e-15, + -0.3291259634750056, + -0.5899399043567829, + -5.801473429063729e-15, + 3.920089208471511e-15, + 0.47675778475722025, + -0.16905397867667005, + 0.3010461090816804, + -9.84633813595521e-16, + 1.4005570374053435e-14, + 0.19628184634539142, + 0.3449216067249908, + 0.35145704911631387 + ], + [ + -7.226857712559586e-17, + 1.776195008676595e-17, + 1.1392548901212323e-16, + -8.943828582255323e-17, + -3.144645361282775e-18, + -7.332999739597383e-17, + -1.341436674138385e-16, + -2.4215136750666306e-15, + -0.02617038672563256, + 7.880497847148807e-17, + 1.7393096197609456e-17, + 0.02196505299484301, + 0.0062261903679280695, + 1.0656050999102642e-16, + 4.545152860814954e-16, + -3.1993375190067283e-17, + 0.02512640162572376, + 4.584455765215653e-15, + 1.149307654095468e-15, + 8.074484048644328e-16, + -2.7799797686202617e-15, + -0.021008304643926216, + 0.01849240274163806, + 2.2749925345939393e-16, + -3.339843345845812e-16, + -1.8053838161993363e-15, + 9.349548527898479e-16, + -0.428431462907117, + 1.7450620777119125e-15, + 0.30400844605712857, + -6.9853615038769564e-15, + 9.754034169582665e-15, + -0.06544844107513838, + 0.37511880482093063, + 2.084348809335437e-15, + -1.0046494513768877e-15, + -2.03415893970138e-15, + -0.5620053241746479, + 0.5903724921428054, + -3.2099043292773115e-14, + -5.3532580357787044e-15, + -3.1516290904673497e-15 + ], + [ + 0.00019941074619584284, + -0.00015248221967125532, + -0.00012143877234843606, + -0.014667558998131736, + -0.013013300483294419, + 0.00981588060578977, + 0.004238730789351605, + 0.02376385002623363, + -2.1430337032580446e-15, + -0.0014798277677331032, + -0.013010859375523414, + -8.788484002947897e-17, + -3.189948175602379e-16, + 0.0037668168452689114, + 0.011391641598689125, + 0.001057945429376511, + -1.3797402895518763e-14, + 0.07703070872139414, + 0.02386187063691323, + 0.032185066465250295, + 0.012551099921569431, + -1.3623118081108904e-15, + -1.93134496370067e-15, + -0.018292147960538956, + 0.09370227874049385, + -0.04999902218526372, + -0.04863375536581155, + 3.721457252022226e-16, + -0.23978860265873322, + 2.248190762285365e-15, + -0.2147888984679387, + -0.06891856104517485, + -4.553093784338745e-15, + -4.996492875999471e-15, + 0.3441493939836149, + -0.4217317128181767, + -0.4600553561120964, + 3.3902570711759357e-15, + -5.479444221123077e-15, + 0.010247862441986071, + -0.6400227827468534, + -0.508696389294046 + ], + [ + -1.0010012371075216, + -1.4061167274331859e-05, + -0.002634855162960031, + -0.003072333750662838, + -3.6777838900388707e-09, + 0.013403477115239966, + 0.0002800171043882835, + 3.42174314914534e-08, + 1.828415108845459e-16, + -1.4254697727694438e-09, + -0.0031921586566836914, + -3.1667454342710296e-16, + 2.7109434910019556e-16, + -0.006714140088455252, + -1.3187519642568316e-07, + 0.026813743598619263, + 9.487176226790105e-16, + 5.363826564664914e-07, + -0.011000166710641062, + -1.0804944544711626e-06, + -0.15379034674636732, + 1.2422426433228657e-14, + -8.202994753713696e-15, + -1.8208843116896523e-06, + 0.3137888500002298, + -2.186734830673696e-06, + -0.746144713260582, + 3.6881817215941515e-16, + 0.17082829008531747, + -1.0264036059144819e-15, + -1.099378504572719e-05, + -0.32824319228101867, + 1.3178042936091564e-15, + 8.874400712403029e-15, + 2.2229677517506466e-07, + 0.21252920618473314, + 1.8594951989113936e-06, + 1.5020446187568966e-15, + -1.095410392776161e-14, + -0.24085642065312207, + -0.3076272177454349, + 2.578586874950061e-06 + ], + [ + -0.001073666524284968, + 2.775159955355845e-07, + 5.2067014207895814e-05, + -0.3928261010239749, + 9.78670864851219e-07, + 0.3267592703330938, + -0.016754263548965715, + -3.971134510536772e-07, + 7.629323098536348e-16, + -5.850253980317882e-07, + 0.07011891156379486, + -4.798654732431997e-16, + 4.440365902538791e-16, + 0.2586230864259066, + 1.872753092548266e-06, + 0.1439961753018607, + 3.287796254717533e-15, + 1.2577434375078693e-06, + -0.03561781282558765, + -3.270545284856942e-06, + -0.3973393350499186, + 3.247011978786863e-14, + -2.1242789876619184e-14, + -4.084482586394365e-06, + 0.7144178354189308, + -4.5220437282501325e-06, + -1.5743060202357466, + 7.618558305029821e-16, + 0.35541431175870897, + -2.126362390024033e-15, + -2.3865816706644173e-05, + -0.706476109653514, + 2.8069854152856658e-15, + 1.909092531241051e-14, + 1.3945214359220683e-07, + 0.49876597863604666, + 4.197157468747397e-06, + 2.9191204888550022e-15, + -1.6970163184254934e-14, + -0.40825570197335304, + -0.47140591990734176, + 4.448851502114274e-06 + ], + [ + 0.005422542454510997, + 6.179581362698963e-06, + 0.0011587834710626544, + -0.33522679680281886, + 1.2778947075702832e-06, + 0.4140429294363816, + -0.01148554172410358, + -1.1286558522690342e-06, + -7.383911191917963e-16, + -1.1948240501864836e-06, + 0.18883584022231414, + 4.066037299984612e-15, + -1.8574847575184068e-15, + 1.3633974812246439, + 1.3686280241303486e-05, + 1.2528727419487546, + 4.330135533611502e-15, + -3.2703123481572116e-06, + 0.24435770589861303, + 9.054051385225935e-06, + 0.9942576899960688, + -8.58197584037287e-14, + 4.24204462571216e-14, + 7.116171655132245e-06, + -0.6353592648622944, + 8.304729786062135e-06, + 3.8935997913690863, + -7.987043673837798e-16, + -0.7485112530871579, + 5.497706137465825e-15, + 4.383116882837187e-05, + 1.3473161464717132, + -7.565805596742324e-15, + -3.743776595696282e-14, + -3.2463879244564806e-06, + -0.4324861618151409, + -2.9437428082140555e-06, + -9.95175267761342e-15, + 8.892774078733299e-14, + 1.7656766928483771, + 2.5189688393158853, + -1.4746712557645627e-05 + ], + [ + -0.0012405591825331481, + 3.192826236833549e-05, + 6.216800881680539e-05, + -0.08035807369646543, + -0.21411453909915645, + -0.15056285373356795, + 0.34455640260343756, + 0.29900043377162355, + -2.920288071700987e-14, + -0.06881953368436597, + -0.23867449166725535, + -9.31914077587288e-16, + -6.571670645461179e-16, + 0.24946663471869274, + 0.3890466055802681, + -0.4765632647384737, + -9.968922372198807e-14, + 0.49211733026135845, + 0.21819015033637623, + 0.13578939019712966, + 0.2829056906383948, + -2.4387416174129088e-14, + 1.2750180344202483e-15, + -0.3020126905968096, + 0.21432691030913387, + 0.11575538244720963, + -0.04202554478662507, + -1.512667471615778e-15, + -0.0329933104822789, + -6.675954954488564e-16, + 0.16876380179674816, + -0.10065637500638636, + 3.735598413449597e-15, + 5.866836096891918e-15, + 0.10737007778830632, + 0.13600506287769687, + 0.19421228025408024, + -1.1214706473772473e-15, + 4.991898201642654e-15, + 0.046051234478171235, + 0.14615714764123386, + 0.22723133334540901 + ], + [ + -0.0012405529266072345, + -3.126450374150054e-05, + 6.250683379774272e-05, + -0.08035688483017414, + 0.21411464831739763, + -0.15056298541313606, + 0.34455541331984424, + -0.2990012073186251, + 2.544386324021274e-14, + 0.06882429994425189, + -0.23867447264020886, + -3.45348151814262e-17, + 1.1082799981697953e-15, + 0.24947208607551075, + -0.38904320428296496, + -0.4765638822525645, + 7.020792865091491e-14, + -0.4921147699637985, + 0.21819219219043381, + -0.13578112258646896, + 0.28290833955801, + -2.1805151215168236e-14, + 2.197475544836594e-14, + 0.3020121510629762, + 0.21433187505141785, + -0.11575538244006105, + -0.04202540613376373, + 7.083229202767394e-16, + -0.03299302879980649, + 6.388346218873917e-16, + -0.16877072939965365, + -0.10064705166120094, + -4.003148700659664e-15, + -4.654970776373855e-16, + -0.1073705580484886, + 0.13600484313293415, + -0.19421047689020757, + -5.811613183570061e-16, + 2.9059758295292124e-15, + 0.046050995715366076, + 0.14615199854888605, + -0.22723205623639917 + ], + [ + 7.550815613087127e-17, + 5.2339525361060326e-17, + -1.5300611039749706e-18, + 6.756925035001615e-16, + -2.373745156555218e-16, + -8.434193249870992e-16, + 3.0740190170240735e-15, + 4.629039219213014e-14, + 0.4994372599878549, + 5.313731410241504e-16, + -2.8106732358294856e-17, + 1.2460317355468467e-07, + -0.47749117640475536, + -1.3118869118643924e-16, + 7.513158216502361e-16, + 8.721148872131923e-15, + -0.7074405315510175, + -1.1201883590235598e-13, + -3.660985519979887e-14, + 4.707201014932263e-16, + -2.4482612763348014e-14, + -6.092557543795121e-06, + 0.626345343259594, + -1.074925396618705e-15, + 7.032300630316149e-15, + -1.0996882614231124e-15, + 3.43732740270206e-16, + -6.724310954594689e-07, + -1.522300644350135e-16, + 0.08821950572216442, + -1.3761501286423276e-15, + 5.716366776447079e-16, + -9.336153280657772e-08, + 0.005938367624195928, + 7.986281274309023e-17, + -7.237733767153121e-17, + 3.7509700516109532e-16, + 0.030995000156080893, + 1.5182021941761492e-07, + -2.760645509165656e-16, + 2.1403722196467027e-16, + 8.842180821794658e-16 + ], + [ + 0.0013559732853018437, + 0.0009299025348614756, + 4.244029083137401e-05, + -0.036791483680718384, + -0.05729551307716413, + -0.06168825848201445, + 0.20442803498585185, + 0.1419526006460149, + -1.3346688229899745e-14, + -0.0021050644139652404, + -0.16202016135964437, + 7.755997964035714e-17, + 3.907061178758958e-16, + 0.5122352745558014, + 0.9035344820940345, + 0.982734420398517, + 2.2851030811213454e-13, + -1.2728625971285636, + -0.08167061421440809, + 0.049798895245451606, + -0.4023187132834646, + 4.024468294715914e-14, + 2.3981561840330035e-15, + 1.0126811528888788, + -0.46904390197305934, + -1.3219387209204014, + 0.7946191815360729, + 6.4682340872018045e-15, + -0.2007942718421548, + 1.7827264354740726e-15, + -0.5510978494901214, + 0.4207765623261231, + -1.3934736080789335e-14, + -2.018894207410266e-14, + -0.3693123482008664, + -0.19345966574150078, + 0.33632343114812774, + -4.420155651135708e-15, + 2.8410638150658376e-14, + 0.6398037375178663, + 0.5044342118765871, + 1.0278499539920471 + ], + [ + 0.0013559716926445275, + -0.0009293976960642831, + 5.2356156238426254e-05, + -0.03679125900143305, + 0.05729573717552413, + -0.06168841870409751, + 0.20442779111033066, + -0.14195325347900944, + 1.0935716334628728e-14, + 0.002108606973596082, + -0.16202102675131277, + 1.3521192389788804e-15, + -6.084610497917691e-16, + 0.5122448806587175, + -0.903524885159391, + 0.982736758302364, + -2.046264669711747e-13, + 1.272859226572418, + -0.08167552425956554, + -0.04981293074079704, + -0.4023147607574893, + 2.914536865395837e-14, + -3.6909964650012043e-14, + -1.0126770990622613, + -0.4690574521653702, + 1.3219420352565974, + 0.7946077849585986, + -6.3098721525600955e-15, + -0.2007939488699487, + 1.4525911963987521e-15, + 0.5511253544307765, + 0.42074210881931334, + 1.0974992851607651e-14, + -1.3320347614512276e-15, + 0.3693106951216751, + -0.19344833162241584, + -0.33632632516572153, + 1.5641207492456734e-15, + 3.336472175288476e-14, + 0.6398055938296223, + 0.5044172209916621, + -1.0278556066458602 + ], + [ + -4.932978655726522e-16, + -3.858716378227519e-17, + 6.123100351749968e-17, + -6.041695399144834e-16, + -9.425043195842682e-17, + -3.5165198267195244e-16, + 1.914640977166994e-15, + 3.0184791532425215e-14, + 0.3175012947196149, + 5.747887747589612e-16, + -3.1420805283428995e-16, + 2.9161126060285523e-07, + -0.45464455509180113, + 2.9110216725185827e-16, + -2.699804769748e-15, + -8.97452969574768e-15, + 0.7068302142428814, + 1.0283250461628334e-13, + 3.666415078782934e-14, + -7.143906284247826e-15, + 5.059468039659547e-14, + 1.0761747902024719e-05, + -1.1789591248852218, + 4.194690256147354e-15, + -1.2402728073932051e-14, + 1.2152697296827239e-15, + -2.789376581549488e-15, + 1.066352740677053e-07, + 1.1283925680884935e-15, + 0.03925829472377486, + -1.7352869352441499e-15, + 1.6869416809188508e-15, + -2.58481577833276e-06, + 0.13023859833727228, + 2.167722824026574e-16, + -4.6611583562281654e-17, + -2.4884536469589583e-15, + -0.4193414189508609, + 2.484279946766648e-06, + -1.1530444961172796e-15, + -1.4153629052424042e-15, + 3.802717825355853e-16 + ], + [ + -4.9139972192500825e-05, + 1.2532259279523831e-06, + 0.0002349391812642442, + 0.009251829938461879, + -7.83312828880416e-09, + -0.004633293819919307, + 0.02255642428900985, + -1.1187615327688963e-07, + -7.307249494900588e-18, + -2.3565475894130017e-07, + 0.029830676238592438, + 6.937990380644147e-17, + -3.7948177260392955e-17, + 0.024687074975616462, + 1.1086946702840418e-07, + -0.09454050432555262, + -1.817558504145973e-15, + 1.545710988864917e-08, + 0.00613443603724553, + -5.049783516558052e-07, + -0.037484812192992975, + 3.2971086103830743e-15, + -1.7941581484042357e-16, + 3.5289263638528985e-08, + -0.08571045670591014, + 1.1846417237607226e-07, + 0.11228811345615068, + 2.148971982345758e-15, + 0.6086569380748019, + -2.4677677934574447e-15, + -5.82590500507937e-06, + -0.182951417682054, + 7.195879909752421e-17, + 5.1526866618719436e-15, + 5.42217326530445e-07, + 0.09406308523800401, + 9.929921354547193e-07, + -1.2623904850965279e-15, + 2.1460774781943605e-14, + 0.522157301587059, + -0.7282819362796469, + 5.833844045641243e-06 + ], + [ + 2.1752424389345974e-16, + 5.029684788106848e-17, + 1.5056354783991496e-16, + -6.901818143555954e-17, + -5.495539909486544e-17, + -7.530817129393245e-17, + 1.0486300913536804e-16, + 2.027340012490082e-15, + 0.021388142856544667, + -3.4998576939614456e-17, + -6.399827764709759e-17, + 0.03073769998788244, + 0.0031017351171815566, + -5.835656643344614e-18, + 2.98846114158256e-16, + 2.6370263257629685e-16, + -0.026625641874227387, + -4.166342503538712e-15, + -1.4846814385475334e-15, + -3.6660053850675167e-16, + -2.3562425808443625e-15, + -0.014351381344463655, + 0.018674785747310848, + 8.639683224695077e-16, + 1.5182863606969735e-16, + -1.4487072436454049e-16, + -7.79319888173439e-16, + -0.46618243745509697, + 1.299029596903125e-16, + -0.5218884376160272, + 2.103759898095543e-15, + -3.3510299093496484e-15, + -0.004814041046211707, + -0.07930951690533286, + 1.6280961228643237e-15, + -5.929643891752847e-16, + -6.104568066148214e-15, + -0.49817580354602725, + -0.5831228159617589, + 2.818900804626549e-14, + 4.587130960989423e-15, + 3.559288125141728e-15 + ], + [ + 0.0001428488476022815, + 1.9436043758412013e-06, + 0.0003643100780017828, + 0.017471740054396914, + -2.8168076362039754e-08, + 0.005902041689138691, + -0.014097693226871166, + -6.208381624593798e-10, + -1.9795894053489494e-16, + -1.434107906548935e-07, + 0.014940250475559188, + 7.083791917842222e-17, + 7.761259929568166e-17, + 0.020883717893617978, + 1.0941969442855063e-07, + -0.0749922980766121, + 6.445240793022765e-16, + -2.1950501686641712e-08, + -0.03785700620950398, + -8.734572358556348e-07, + -0.06829986604067217, + 6.476662697268637e-15, + -1.6283074560511371e-15, + 3.0772918165903135e-07, + -0.1347473303583918, + 5.843660360841642e-07, + 0.15702177215140115, + -1.3410985851144486e-15, + -0.3403731399455043, + 1.2972151036925361e-15, + -1.1494400704410922e-05, + -0.29633659811132707, + -1.615721246409904e-15, + 5.1871747494326175e-15, + -2.3202762266790163e-06, + 0.6168826841232165, + 3.46058918279828e-06, + 1.2150919913506634e-15, + -2.7383966979682646e-14, + -0.46821471299791445, + -0.6359807074320812, + 4.104162972750396e-06 + ], + [ + -8.31616302355406e-17, + 1.5390985820428067e-16, + -8.029479617064937e-17, + 5.65497348634005e-17, + 2.2467173678619488e-17, + -6.654482314491992e-17, + 1.6868291630511466e-16, + 2.144174518476808e-15, + 0.021388331658808026, + 1.142629404874943e-16, + 1.5663726775540034e-16, + -0.03073768980583212, + 0.00310179026247711, + 7.675970593775375e-17, + 4.579555170867327e-16, + 4.2387443241620065e-16, + -0.02662599700566266, + -4.497521198588121e-15, + -1.6509121171145228e-15, + 1.6380923132653128e-16, + 6.246024714100529e-16, + 0.014351238047080328, + 0.018674867070117777, + 9.85192009133767e-16, + 1.9652093408234633e-16, + 2.427627793232047e-15, + 5.18079899597404e-16, + 0.4661836112042665, + -4.2864112100471076e-15, + -0.5218867104693762, + 1.990730870216727e-16, + -7.098139819591328e-16, + 0.00481776677788859, + -0.0793089766590969, + 7.577646201689517e-16, + -1.05164554990519e-15, + -2.14778068438821e-15, + -0.49817067647520435, + 0.583128384335261, + -3.114073256964781e-14, + -5.355931475673746e-15, + -1.236105130627625e-15 + ], + [ + -1.258212042779213e-09, + -0.0003476842777311292, + 1.852122554267521e-06, + -1.1488269542528357e-07, + -0.03507609137664523, + -1.763644820865269e-08, + 1.1399252056516555e-07, + 0.02158794860148395, + -1.869055864127683e-15, + -0.029710130493980757, + -1.929384017379902e-07, + -6.322770343923514e-17, + -1.9606563752652493e-16, + 2.3001979828800917e-07, + -0.03297849064749296, + 3.918052259176637e-07, + 3.243558202731441e-15, + -0.02616486300903077, + 3.0651101548176644e-07, + 0.1260946536842911, + -1.3505188482803279e-06, + 1.838505007665779e-16, + -2.167097189346346e-15, + -0.09586832737159776, + -6.037601511145374e-07, + -0.12401770920517666, + 8.234648582124874e-07, + -6.12044408126081e-16, + 4.3489906716544065e-08, + -1.678742502355663e-15, + -0.4167292200415101, + 1.1885870168070753e-05, + -1.0043225081069346e-14, + -3.6821696713595675e-15, + -0.27962259180562377, + -6.92740899073739e-07, + -0.4351843272595588, + 6.075533741215819e-15, + 5.656753018663876e-15, + -9.307983166908034e-08, + 8.211849831382716e-06, + 1.0337225104750014 + ], + [ + -0.0017613300505372252, + -0.7039736902274834, + 0.7112943205776673, + -9.398298676863867e-05, + -0.0012603242328684708, + -0.0006688484801919751, + 0.004981847646505617, + -0.0015708111746075254, + -8.017393986435142e-17, + 0.0006765724056206014, + -0.007066406468787232, + 5.051739225088102e-16, + 7.147785599797437e-17, + 0.01256555217393601, + -0.005897784564322523, + 0.11191680173717093, + 8.106921876791239e-15, + -0.06687742323188318, + 0.0765178001208526, + 0.0030450520715755614, + -0.10550866456335226, + 8.777129317747287e-15, + -7.45794380354549e-15, + -0.03319048947718257, + 0.3346417862946701, + 0.5631376955590275, + 0.3587062263971887, + -1.9163775248931924e-15, + -0.021499153190363295, + 1.1532468664089402e-15, + -0.02094361080996321, + 0.029171806206018353, + -8.565931156225149e-16, + 4.167620957623335e-16, + -0.06835062933105666, + 0.06886646758261143, + -0.16677440399611618, + 6.930759715500321e-16, + 6.151704134479237e-15, + 0.10832874820663324, + 0.15397612051762954, + -0.19125184286022504 + ], + [ + 0.0001593217709164905, + -0.0006747553872258788, + 0.00023769096887512508, + -0.19856170361980904, + 0.311361515925907, + -0.2707031847860684, + -0.10910523548817863, + 0.16497737909353788, + -1.5204554738056677e-14, + -0.0050140495157909816, + -0.010430691644906108, + 9.8617158714887e-16, + 4.270795952617853e-17, + -0.1014766390289783, + 0.13729603293912887, + 0.2366542953552118, + 2.7724634382800118e-14, + -0.21593922586146483, + 0.18581388099238663, + 0.0529349494515786, + -0.2976215243986164, + 2.5241414156809218e-14, + -1.9852989964824295e-14, + -0.08135491813088488, + 0.8151188642566387, + 1.249137935968453, + 0.7738334745615227, + -4.137369421713338e-15, + -0.024671688227025104, + 2.4212849067939743e-15, + -0.08221630174079354, + 0.027670402570450437, + -2.6556892358849585e-15, + 1.8870870495248756e-15, + -0.17621681786467513, + 0.18129062162534024, + -0.3692561999846878, + 1.9190020035777542e-15, + 1.13980228989775e-14, + 0.19470984672436598, + 0.23910253489418548, + -0.31165435138767184 + ], + [ + -0.0015896791394839176, + 0.0032541967805489, + -0.002146170176443986, + -0.12583065669092253, + 0.3097280331221159, + -0.3605655553736356, + -0.2406226796125805, + 0.3002873533106203, + -2.5012688839749276e-14, + -0.00407998089672773, + 0.053104544602295994, + -2.9950598304183213e-15, + 4.1141499610879074e-16, + -0.6155690213224254, + 0.8049304065404382, + -0.882187800625097, + 1.215698629222247e-13, + -0.7103274244204719, + -0.19059895684270486, + 0.12124437842551516, + 0.17934033079892978, + -1.0722701928479029e-14, + 2.702389782825458e-14, + 0.6695656170654409, + -0.5551065293748615, + -2.4262136481857812, + -1.9781506261994595, + 9.630084792624026e-15, + 0.304599974092615, + -4.991164705822913e-15, + -0.3496132432462423, + -0.5385003009817542, + -5.004483914614567e-15, + 4.902566723803469e-15, + -0.0793584126475074, + 0.025749455725204057, + 0.6647640764611239, + -8.079919194652267e-16, + -4.8772971474900233e-14, + -0.9028036202143551, + -1.24053396886872, + 1.423970854736291 + ], + [ + -2.1302774418900985e-05, + -0.0003971703599785441, + 0.00026800634201995727, + -0.03348923539414831, + 0.0043591580205813085, + -0.0230017080057999, + 0.25673848846013386, + 0.03149573189863999, + -4.6724734535603716e-15, + 0.4933053220279898, + 0.3574203671885523, + 9.09828247095257e-16, + 9.6092234818424e-16, + -0.15090936059797616, + -0.007334298888454495, + 0.16426080702326465, + -8.403896948094573e-14, + 0.3071745299874465, + 0.6223888108789958, + 0.2357842560421918, + -0.02368051610147158, + 2.8689432988186593e-15, + -4.363279703678358e-15, + 0.5232292110330152, + -0.15614284277523915, + 0.09889137060334598, + -0.07988555297001854, + 2.1063143481847968e-16, + -0.00978630273172361, + 7.374641321923923e-16, + -0.09251980940178464, + -0.017162060020486373, + -1.9947811514246456e-15, + -2.7362210660381774e-16, + -0.1319325975678938, + 0.0452679184115815, + -0.009531243156416808, + 7.93606378920358e-16, + -2.2964872962877536e-15, + -0.035531823986098464, + -0.04317345137402883, + 0.04334963669813458 + ], + [ + 0.00011879352608077455, + 0.0012530919370605737, + -0.0011804004068441964, + 0.10376441173300982, + -0.09105189500507013, + -0.10939513892070361, + -0.12383098009361516, + 0.32019103586597136, + -2.9582825975857715e-14, + -0.0013022756581949938, + 0.2565230937526246, + 9.133280487401617e-16, + 2.697929238486124e-16, + 0.3386787088090443, + -0.32361278830264295, + -0.3670918572329757, + 2.931036371882568e-14, + -0.27522623007514957, + 0.058447134435822966, + 0.5738949787331941, + -0.5093974864886626, + 4.438337414083397e-14, + -2.6073330202405912e-14, + -0.07146968210957057, + -0.08826338763038255, + -0.08788227419365917, + -0.0026431002911249894, + 4.938956140446418e-16, + -0.10055125925046259, + 5.155904499309114e-16, + 0.17375779596770438, + 0.15836636379518715, + 4.445705777536366e-15, + -3.397591938350395e-16, + 0.1459447318632273, + -0.1978706059183265, + 0.1652906721149198, + -2.251278140093081e-15, + 2.3594307259089997e-15, + 0.06377878688893793, + 0.1893386208194984, + -0.17852320218695433 + ], + [ + -1.4309566617794539e-16, + -5.2240027241748233e-17, + -5.366488341965129e-18, + 1.8229732623175092e-16, + -2.108601910021458e-16, + -3.000178277328976e-16, + 8.748015708749215e-16, + 2.4565077608093327e-14, + 0.2610555058774571, + -7.357794379994173e-16, + -6.817950429749327e-16, + 0.4553432165137196, + 0.3730606237681433, + 7.059495403916955e-17, + 9.537893497896494e-16, + 3.3947109364688394e-15, + -0.4714092201251421, + -8.760264753351613e-14, + -2.0801999445627096e-14, + -1.4773032001525013e-14, + -3.529203938362898e-14, + -0.6852502003098183, + -0.4891013148244063, + 2.3277002625123767e-15, + -4.961239615007981e-15, + -1.4425026532856373e-16, + -5.967944324567362e-16, + 0.06475946353947955, + -3.9910486362367187e-17, + 0.04028119991191867, + -5.321090633512867e-16, + 7.900843068395309e-16, + -0.02436319440380709, + 0.03655039702636404, + -3.597973447746488e-17, + 4.880790729097958e-16, + -8.524372645440926e-16, + -0.03165369473305965, + -0.006503798265703627, + 1.885920606665614e-16, + -1.4924679300573276e-16, + -1.0611587489666756e-16 + ], + [ + -0.00046350996362259667, + 0.0003885018456417203, + -8.159541825817855e-05, + -9.14057740344396e-06, + 0.008162599124011832, + -0.01505500738868116, + 0.12574424464946918, + 0.013407918736240537, + -1.7337494237266964e-15, + 0.3497880070524966, + 0.2828900593441216, + 5.7188365573056e-16, + -6.054745908615651e-16, + -0.30664645916641126, + 0.05368063523913842, + -0.4637992976244009, + 7.670388878568893e-14, + -0.27998311552236155, + -0.6256482027368823, + -0.1874758885217344, + 0.14637553145442075, + -1.419859801191204e-14, + 6.686266845444978e-15, + -0.8507416869259571, + 0.36501640717401546, + -0.22869121306322182, + -0.30517305969622777, + -2.870157473950213e-16, + 0.00385794573589821, + -1.86161195430529e-15, + 0.10897332214071645, + -0.15420549795525237, + 2.6030974633703204e-15, + 4.803122880621867e-15, + 0.20443688328511134, + 0.026024320631515094, + -0.015437585917301242, + 4.958432965169636e-16, + -1.4271276077658225e-14, + -0.32481890304458455, + -0.17686013475998155, + 0.14111544848111712 + ], + [ + 0.0006925141255544204, + -0.0013552560245302344, + 0.0007270548487615724, + 0.01605473376738698, + -0.039077630320965055, + -0.03456730954087202, + -0.0385872484115535, + 0.1609208048569433, + -1.6470307511335543e-14, + -0.007930514214979542, + 0.16281488537282898, + 2.0336769806409827e-15, + -3.601423258571867e-16, + 0.6784299591451881, + -0.811048716772857, + 0.7977388976037361, + -1.1386588208137066e-13, + 0.798947382035201, + 0.020023567587810542, + -0.8739359954156816, + 0.7860550593396991, + -7.098191700530822e-14, + 3.3544565797600985e-14, + -0.2423979968761302, + 0.11716997025686536, + 1.0671430577996348, + 0.920318212694082, + -5.037957127381846e-15, + -0.12728962133089805, + 1.555773677688173e-15, + 0.11443710770481412, + 0.19799111997630087, + 6.449095090802989e-16, + -4.6950804854848115e-15, + 0.0021971626684818783, + 0.09251292626446021, + -0.45048405367278255, + 7.536831655314065e-16, + 2.6019628748097452e-14, + 0.45967445538372714, + 0.7609855470593834, + -0.8193502170345943 + ], + [ + 9.941902557767123e-19, + 3.9524331179906395e-17, + -4.013368368931335e-18, + 1.1609496276492806e-16, + -1.838894341763903e-16, + -6.335561754843437e-17, + 7.193592823879593e-16, + 1.3634064274756099e-14, + 0.1541764160964085, + -7.402164047715438e-16, + -1.8849187160488729e-16, + 0.36809088937681844, + 0.38464811389448744, + -4.052407285630491e-16, + 2.5573287919020296e-16, + -1.1173935659694867e-15, + 0.32292963219866677, + 6.819808298624103e-14, + 1.2453401034743436e-14, + 1.7845665429410595e-14, + 3.0251925732049343e-14, + 0.7921410947337875, + 0.8326170925760483, + -4.374593187199196e-15, + 8.925744519623895e-15, + 1.2128871022414656e-15, + 2.5246631298329e-15, + 0.07140221889563941, + -5.084461983430804e-16, + 0.02433954162481145, + 7.300547892713681e-16, + -2.761330350896548e-16, + 0.03269227221591637, + -0.072093477112905, + -3.5874932673186177e-16, + -4.306813479123662e-16, + 2.137318266269459e-15, + 0.22823163910454128, + 0.1427463422107145, + -6.188596483025551e-15, + -9.481149016852375e-17, + -1.3310820671260475e-15 + ], + [ + -2.536791514165615e-05, + -7.368484754010199e-05, + 6.716801033864665e-05, + 0.00895130898742157, + -0.004046033979091001, + 0.0013900974463644483, + -0.02112087593129741, + 0.004106788201742911, + -5.061768783329361e-16, + -0.017587004326387483, + -0.0017505281535641923, + -4.662696804922696e-17, + 3.3157019155371244e-17, + -0.013654234082026286, + -0.011284963861219301, + 0.03780073564003595, + -4.106738883612274e-15, + 0.027590776215515964, + -0.012116536999196346, + 0.024047506355676644, + 0.009321706323079966, + -5.688376173091981e-16, + -8.916878172122175e-16, + -0.08915044275324883, + -0.0015498095626796055, + -0.02962000582878333, + -0.08654003570732069, + -1.400820347524685e-15, + -0.30899239946043844, + -6.859316519965465e-16, + -0.4599270914110555, + 0.14306982140900384, + -1.0419282671209046e-14, + -9.719160397836612e-15, + -0.30469993610010165, + 0.22640696575645444, + 0.3124723212482826, + -3.948702801982127e-15, + 2.935248745243979e-14, + 0.6536776133688468, + 0.050902422774331955, + -0.38431112364017084 + ], + [ + -5.887023056144293e-18, + -9.197477353987915e-17, + 1.9156727911589254e-16, + -1.2986286351701265e-16, + -8.681171385924841e-17, + -1.1640376421991494e-18, + -1.0471343086374296e-16, + -2.35981957581066e-15, + -0.026170225653117465, + 5.943576761660668e-17, + 3.0081166207421423e-17, + -0.02196507412962472, + 0.006226199355940923, + 8.636686254927274e-18, + 8.168617534943777e-17, + -4.942145693620185e-16, + 0.025126038437548966, + 5.9640824436240554e-15, + 5.206793311754575e-16, + -4.8808857014645754e-17, + 8.281298065950059e-16, + 0.02100801856152951, + 0.01849284373650161, + -1.0572341365605352e-15, + -4.292535202826627e-16, + 1.8363372150960536e-15, + -9.729563974303852e-16, + 0.4284300906913619, + -4.830393855232485e-16, + 0.30400956679385815, + -9.13373061649192e-15, + 6.463303314248677e-15, + 0.06543408002086683, + 0.37512236450482184, + 1.4446105371679421e-15, + 4.975664899759549e-16, + -4.750072127325573e-15, + -0.5620111022828179, + -0.5903661724870815, + 2.8060619567726474e-14, + 4.898174389848054e-15, + 4.9248423714119294e-15 + ], + [ + -0.00016279249340948624, + 4.061266495791603e-05, + -0.00012951174303520822, + 0.009312477658289459, + -0.007623469438667967, + -0.002758711836439576, + -0.0077699786507163665, + 0.012784397040472528, + -1.180977763205326e-15, + -0.004289207979364994, + 0.004828167781991947, + 1.7916372162398945e-16, + -4.252282720005893e-17, + -0.00033343081619599096, + 0.0070407864001065905, + 0.021173185488919576, + -6.206560211629025e-15, + 0.03691280246467279, + -0.005805594286721782, + 0.01830423670489943, + -0.011474100087736253, + 1.0287660867640863e-15, + -8.733346564627528e-16, + -0.04235451649188022, + -0.060922728035432507, + -0.09513638194129317, + 0.08917337934428607, + 1.2671344145774996e-16, + -0.10718793802562869, + -6.294815442539691e-17, + 0.3290900863609058, + -0.5899640731421419, + 8.400889222231962e-15, + 2.1931206010522772e-14, + -0.47675330512334513, + -0.16905318794989735, + -0.3010456721187955, + 3.8738272911470583e-16, + 1.3645733716227054e-14, + 0.19628210327386886, + 0.3449160183381891, + -0.35146170675284094 + ], + [ + -1.0372416947701155e-16, + 2.1540387776205482e-17, + -4.677855207288378e-17, + -2.7975603887133102e-18, + -3.677958873669241e-17, + -1.1740023438840006e-16, + 2.1018893665934972e-17, + 6.02133592655218e-16, + 0.006718527095527946, + -1.5015341087039998e-17, + 3.6404112862690624e-17, + 0.0049519320418373175, + -0.0004762870104316398, + 2.843856645510013e-17, + -2.6652439424657585e-16, + 2.8689176484437154e-16, + -0.0035581622472791777, + -6.503166996488833e-16, + 1.7268970566068356e-16, + -5.039927298122869e-16, + -1.5181498464797696e-15, + -0.016910542174437996, + 0.010398523311268561, + 5.260383858423728e-16, + 4.946067784365688e-17, + -1.5704721638779545e-15, + 2.167455325738879e-15, + -0.03747918474317207, + -1.6689294000632852e-15, + -0.26867236575213177, + -2.5715954874157724e-14, + 1.4198185070048624e-14, + 0.7042697017754728, + 0.5960241584192306, + 4.5516784663927806e-15, + 1.0401229438979508e-15, + 2.4950288471129013e-15, + 0.278480290703868, + 0.0598424296121514, + -4.0397850821947335e-15, + -1.1245396117706782e-15, + -2.327635838878825e-15 + ], + [ + -0.000199408835729763, + -0.0001511802900839243, + 0.0001230597142560452, + 0.014667454698213456, + -0.01301335452150573, + -0.009815829780834154, + -0.004238633050417151, + 0.02376381235675366, + -2.2038939010631763e-15, + -0.0014800310264468776, + 0.013010922913857843, + 4.1869931377365855e-17, + -4.1251749764666887e-16, + -0.0037669387273488153, + 0.011391536794820226, + -0.0010579372303174575, + -1.241349538406943e-14, + 0.07703009871135336, + -0.023862275066472382, + 0.03218456585682706, + -0.012551978277172233, + 1.5135176991456084e-15, + -1.5740904032765052e-15, + -0.018291448810810712, + -0.09370277279435005, + -0.04999921100720285, + 0.048634073569554424, + 1.6927595621620924e-15, + 0.23978843466263194, + -5.277010638132867e-16, + -0.21478636234351697, + 0.06893241986380544, + -5.5680485514386706e-15, + -7.377347765510867e-15, + 0.34414605722690944, + 0.42173804513701674, + -0.46005166393851354, + 2.133614791380758e-15, + 3.419828578416071e-15, + -0.010246627554700865, + 0.6400145339250181, + -0.5087048653287369 + ] + ] + }, + "energies": { + "alpha": [ + -20.912095677226034, + -20.698954211524843, + -20.698874850321015, + -1.7869159821383107, + -1.452755810943348, + -1.0829736925615612, + -0.8424362452085107, + -0.8031249234718821, + -0.7986493350461514, + -0.5574646661379341, + -0.5423586627843083, + -0.48308340721880605, + -0.026459503025581078, + 0.3422158994645042, + 0.43322011905959124, + 0.9967325996823357, + 1.0647371545979665, + 1.0709558112588546, + 1.0784073869449264, + 1.1548127598166118, + 1.1902888601199548, + 1.2095189128688195, + 1.2332309931520804, + 1.310781435410077, + 1.3413762323019054, + 1.8907610305565312, + 1.977492080450955, + 2.2453676115305212, + 2.3283465778508328, + 2.4237421653127744, + 2.8139608095421127, + 2.8464716114001125, + 2.8662958317511555, + 2.886107281674449, + 2.933250734329282, + 3.0547697393461246, + 3.2094861092776363, + 3.4634759948998464, + 3.6408201164722054, + 3.7049955218926467, + 4.163259711028222, + 4.202051274874996 + ], + "beta": [ + -20.912095677226034, + -20.698954211524843, + -20.698874850321015, + -1.7869159821383107, + -1.452755810943348, + -1.0829736925615612, + -0.8424362452085107, + -0.8031249234718821, + -0.7986493350461514, + -0.5574646661379341, + -0.5423586627843083, + -0.48308340721880605, + -0.026459503025581078, + 0.3422158994645042, + 0.43322011905959124, + 0.9967325996823357, + 1.0647371545979665, + 1.0709558112588546, + 1.0784073869449264, + 1.1548127598166118, + 1.1902888601199548, + 1.2095189128688195, + 1.2332309931520804, + 1.310781435410077, + 1.3413762323019054, + 1.8907610305565312, + 1.977492080450955, + 2.2453676115305212, + 2.3283465778508328, + 2.4237421653127744, + 2.8139608095421127, + 2.8464716114001125, + 2.8662958317511555, + 2.886107281674449, + 2.933250734329282, + 3.0547697393461246, + 3.2094861092776363, + 3.4634759948998464, + 3.6408201164722054, + 3.7049955218926467, + 4.163259711028222, + 4.202051274874996 + ] + }, + "has_overlap_matrix": true, + "inactive_space_indices": { + "alpha": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 9, + 10 + ], + "beta": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 9, + 10 + ] + }, + "is_restricted": true, + "num_atomic_orbitals": 42, + "num_molecular_orbitals": 42, + "type": "Orbitals", + "version": "0.1.0" + } + }, + "container_type": "sci", + "is_complex": false, + "version": "0.1.0", + "wavefunction_type": "self_dual" + }, + "container_type": "sci", + "version": "0.1.0" +} diff --git a/python/tests/test_state_preparation.py b/python/tests/test_state_preparation.py index b8ec42d70..55b9818b6 100644 --- a/python/tests/test_state_preparation.py +++ b/python/tests/test_state_preparation.py @@ -28,7 +28,9 @@ _find_pivot_row, _is_diagonal_matrix, _perform_gaussian_elimination, + _perform_gaussian_elimination_forward_only, _reduce_diagonal_matrix, + _ref_to_staircase, _remove_all_ones_rows_with_x, _remove_duplicate_rows_with_cnot, _remove_zero_rows, @@ -889,7 +891,8 @@ def test_reduce_diagonal_matrix() -> None: non_diagonal_m = np.array([[1, 0], [1, 0]], dtype=np.int8) elimination_results = _reduce_diagonal_matrix(non_diagonal_m, row_map, col_map, operations) - assert all(non_diagonal_m[i, j] == elimination_results.reduced_matrix[i, j] for i in range(2) for j in range(2)) + assert elimination_results.reduced_matrix.shape == (1, 2) + assert np.array_equal(elimination_results.reduced_matrix[0], non_diagonal_m[0]) def test_gf2x_edge_cases(): @@ -952,3 +955,243 @@ def test_gf2x_with_tracking_edge_case_pseudo_diagonal(): # Should have some operations recorded assert len(elimination_results.operations) > 0, "Should have recorded some operations" + + +def test_forward_only_produces_upper_triangular(): + """Forward-only elimination produces row echelon form (zeros below each pivot).""" + matrix = np.array([[1, 1, 0], [1, 0, 1], [0, 1, 1]], dtype=np.int8) + m_result, _, _ = _perform_gaussian_elimination_forward_only(matrix, [0, 1, 2], []) + + for r in range(m_result.shape[0]): + nz = np.flatnonzero(m_result[r]) + if nz.size == 0: + continue + pivot_col = int(nz[0]) + assert np.all(m_result[r + 1 :, pivot_col] == 0) + + +def test_forward_only_does_not_back_substitute(): + """Forward-only differs from full RREF — above-diagonal entries survive.""" + matrix = np.array([[1, 1, 0], [0, 1, 1], [1, 0, 1]], dtype=np.int8) + m_fwd, _, _ = _perform_gaussian_elimination_forward_only(matrix, [0, 1, 2], []) + m_full, _, _ = _perform_gaussian_elimination(matrix, [0, 1, 2], []) + assert not np.array_equal(m_fwd, m_full) + + +def test_forward_only_reconstruction(): + """Reversing recorded CNOTs on the REF result recovers the original matrix.""" + matrix = np.array( + [[1, 0, 1, 1], [0, 1, 1, 0], [1, 1, 0, 1], [0, 0, 1, 1]], + dtype=np.int8, + ) + row_map = list(range(4)) + m_result, rm_result, cnot_ops = _perform_gaussian_elimination_forward_only(matrix, row_map, []) + + reconstructed = np.zeros_like(matrix) + for i, orig in enumerate(rm_result): + reconstructed[orig] = m_result[i] + for target, control in reversed(cnot_ops): + reconstructed[target] ^= reconstructed[control] + + assert np.array_equal(reconstructed, matrix) + + +def test_forward_only_row_swap_tracking(): + """Row swaps needed for pivoting are reflected in the returned row_map.""" + matrix = np.array([[0, 1], [1, 0]], dtype=np.int8) + _, rm_result, _ = _perform_gaussian_elimination_forward_only(matrix, [0, 1], []) + assert rm_result[0] == 1 + assert rm_result[1] == 0 + + +def test_forward_only_large_rank_deficient(): + """Rank-4 matrix: REF has exactly 4 non-zero rows and 2 zero rows.""" + matrix = np.array( + [ + [1, 0, 1, 0, 1, 1, 0, 0], + [0, 1, 0, 1, 0, 0, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], # row 0 XOR row 1 + [1, 0, 1, 0, 1, 1, 0, 0], # duplicate of row 0 + [0, 0, 0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 1], + ], + dtype=np.int8, + ) + m_ref, rm, ops = _perform_gaussian_elimination_forward_only(matrix, list(range(6)), []) + non_zero = int(np.sum(np.any(m_ref, axis=1))) + assert non_zero == 4 + + # Verify REF property: below each pivot is all zero + for r in range(m_ref.shape[0]): + nz = np.flatnonzero(m_ref[r]) + if nz.size == 0: + continue + assert np.all(m_ref[r + 1 :, int(nz[0])] == 0) + + # Reconstruction must recover original + reconstructed = np.zeros_like(matrix) + for i, orig in enumerate(rm): + reconstructed[orig] = m_ref[i] + for target, control in reversed(ops): + reconstructed[target] ^= reconstructed[control] + assert np.array_equal(reconstructed, matrix) + + +def test_forward_only_wide_matrix(): + """Wide matrix test: more columns than rows, full GF(2) rank 3.""" + matrix = np.array( + [ + [1, 0, 0, 1, 1, 0, 1, 0, 1, 1], + [0, 1, 0, 0, 1, 1, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 1, 1, 1, 0, 0], + ], + dtype=np.int8, + ) + m_ref, _, ops = _perform_gaussian_elimination_forward_only(matrix, list(range(3)), []) + + # Already in REF (identity-like left block), should need 0 ops + assert len(ops) == 0 + assert np.array_equal(m_ref, matrix) + # All 3 rows non-zero + assert int(np.sum(np.any(m_ref, axis=1))) == 3 + + +def test_ref_to_staircase_fills_above_diagonal(): + """Every above-diagonal entry in a pivot column becomes 1.""" + matrix = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 1]], dtype=np.int8) + m_result, _, _ = _ref_to_staircase(matrix, [0, 1, 2], []) + + pivot_cols = [int(np.flatnonzero(m_result[r])[0]) for r in range(3)] + for j_idx, pc in enumerate(pivot_cols): + for i_idx in range(j_idx): + assert m_result[i_idx, pc] == 1 + + +def test_ref_to_staircase_already_done_no_ops(): + """If the matrix is already staircase, no CX ops are emitted.""" + staircase = np.array([[1, 1, 1], [0, 1, 1], [0, 0, 1]], dtype=np.int8) + _, _, ops = _ref_to_staircase(staircase, [0, 1, 2], []) + assert [op for op in ops if op[0] == "cx"] == [] + + +def test_ref_to_staircase_reconstruction(): + """Reversing the CX ops on the staircase result recovers the input REF.""" + ref_matrix = np.array( + [[1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 0]], + dtype=np.int8, + ) + row_map = [2, 5, 8] + m_result, rm_result, ops = _ref_to_staircase(ref_matrix, row_map, []) + + reconstructed = m_result.copy() + for op_name, args in reversed(ops): + if op_name == "cx": + target, control = args + reconstructed[rm_result.index(target)] ^= reconstructed[rm_result.index(control)] + + assert np.array_equal(reconstructed, ref_matrix) + + +def test_ref_to_staircase_5x8_complex(): + """5-row REF with non-contiguous pivot columns: staircase fills all above-diagonal pivot entries.""" + # Pivots at columns 0, 2, 3, 5, 7 — gaps at cols 1, 4, 6. + ref = np.array( + [ + [1, 1, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0], + [0, 0, 0, 1, 1, 0, 0, 1], + [0, 0, 0, 0, 0, 1, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 1], + ], + dtype=np.int8, + ) + # Verify it's valid REF before transforming + for r in range(ref.shape[0]): + nz = np.flatnonzero(ref[r]) + if nz.size: + assert np.all(ref[r + 1 :, int(nz[0])] == 0) + + row_map = [0, 3, 5, 7, 9] + m_result, rm_result, ops = _ref_to_staircase(ref, row_map, []) + + # Check staircase property: all above-diagonal pivot entries are 1 + pivot_cols = [] + for r in range(m_result.shape[0]): + nz = np.flatnonzero(m_result[r]) + if nz.size: + pivot_cols.append(int(nz[0])) + for j_idx, pc in enumerate(pivot_cols): + for i_idx in range(j_idx): + assert m_result[i_idx, pc] == 1 + + # CX ops should use the row_map indices, not matrix row indices + for op_name, args in ops: + if op_name == "cx": + assert args[0] in row_map + assert args[1] in row_map + + # Reconstruction: undo CX ops to recover original REF + reconstructed = m_result.copy() + for op_name, args in reversed(ops): + if op_name == "cx": + target, control = args + reconstructed[rm_result.index(target)] ^= reconstructed[rm_result.index(control)] + assert np.array_equal(reconstructed, ref) + + +def test_forward_only_then_staircase_end_to_end(): + """Full pipeline: raw matrix, forward-only REF, staircase, verify properties.""" + # A realistic-ish 5x6 binary matrix (5 qubits, 6 determinants) + matrix = np.array( + [ + [1, 0, 1, 1, 0, 1], + [0, 1, 1, 0, 1, 1], + [1, 1, 0, 0, 1, 0], + [0, 0, 1, 1, 1, 0], + [1, 0, 0, 1, 0, 0], + ], + dtype=np.int8, + ) + row_map = list(range(5)) + + # Step 1: forward-only to REF + m_ref, rm_ref, cnot_ops = _perform_gaussian_elimination_forward_only(matrix, row_map, []) + + # Verify REF + for r in range(m_ref.shape[0]): + nz = np.flatnonzero(m_ref[r]) + if nz.size == 0: + continue + assert np.all(m_ref[r + 1 :, int(nz[0])] == 0) + + # Remove zero rows + non_zero_mask = np.any(m_ref, axis=1) + ref_nz = m_ref[non_zero_mask] + rm_nz = [rm_ref[i] for i in range(len(rm_ref)) if non_zero_mask[i]] + + # Step 2: staircase conversion + ops_so_far = [("cx", (t, c)) for t, c in cnot_ops] + m_stair, rm_stair, all_ops = _ref_to_staircase(ref_nz, rm_nz, ops_so_far) + + # Verify staircase: every above-diagonal pivot entry is 1 + pivot_cols = [] + for r in range(m_stair.shape[0]): + nz = np.flatnonzero(m_stair[r]) + if nz.size: + pivot_cols.append(int(nz[0])) + for j_idx, pc in enumerate(pivot_cols): + for i_idx in range(j_idx): + assert m_stair[i_idx, pc] == 1 + + # Verify combined reconstruction recovers original matrix + reconstructed = np.zeros_like(matrix) + for i, orig in enumerate(rm_stair): + reconstructed[orig] = m_stair[i] + + # Undo all ops in reverse (staircase CX ops first, then forward-elim CX ops) + for op_name, args in reversed(all_ops): + if op_name == "cx": + target, control = args + reconstructed[target] ^= reconstructed[control] + + assert np.array_equal(reconstructed, matrix) diff --git a/python/tests/test_state_preparation_binary_encoding.py b/python/tests/test_state_preparation_binary_encoding.py new file mode 100644 index 000000000..69af221fd --- /dev/null +++ b/python/tests/test_state_preparation_binary_encoding.py @@ -0,0 +1,329 @@ +"""Tests for the sparse isometry with binary encoding state preparation. + +All tests live in a single class ``TestSparseIsometryBinaryEncoding``. +Two main tests exercise the full ``run()`` → ``Circuit`` → ``estimate()`` +pipeline on the ozone SCI wavefunction and random wavefunctions. The +remaining tests cover settings, single-determinant fast-path, scaling, +``_encode_gf2x_ops_for_qs`` unit tests, and algorithm metadata. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import numpy as np +import pytest +import qsharp + +from qdk_chemistry.algorithms import create +from qdk_chemistry.algorithms.state_preparation import SparseIsometryBinaryEncodingStatePreparation +from qdk_chemistry.algorithms.state_preparation.sparse_isometry import gf2x_with_tracking +from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import _encode_gf2x_ops_for_qs +from qdk_chemistry.data import CasWavefunctionContainer, Circuit, Configuration, Wavefunction +from qdk_chemistry.plugins.qiskit import QDK_CHEMISTRY_HAS_QISKIT +from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer + +from .reference_tolerances import float_comparison_absolute_tolerance, float_comparison_relative_tolerance +from .test_helpers import create_test_orbitals + + +def _hf_determinant(n_alpha: int, n_beta: int, n_orbitals: int) -> np.ndarray: + """Build the Hartree-Fock reference determinant [alpha|beta].""" + det = np.zeros(2 * n_orbitals, dtype=np.int8) + det[:n_alpha] = 1 + det[n_orbitals : n_orbitals + n_beta] = 1 + return det + + +def _random_excitation(det: np.ndarray, n_orbitals: int, rng: np.random.Generator) -> np.ndarray | None: + """Apply a random excitation independently in alpha and beta channels.""" + new_det = det.copy() + for channel_start in (0, n_orbitals): + channel = det[channel_start : channel_start + n_orbitals] + occupied = np.where(channel == 1)[0] + virtual = np.where(channel == 0)[0] + if len(occupied) == 0 or len(virtual) == 0: + continue + order = rng.integers(0, min(len(occupied), len(virtual)) + 1) + if order == 0: + continue + occ = rng.choice(occupied, size=order, replace=False) + vir = rng.choice(virtual, size=order, replace=False) + new_det[channel_start + occ] = 0 + new_det[channel_start + vir] = 1 + return None if np.array_equal(new_det, det) else new_det + + +def _determinants_to_configs(matrix: np.ndarray, n_orbitals: int) -> list[str]: + """Convert (n_dets, 2*n_orbitals) occupation matrix to config strings.""" + mapping = {(1, 1): "2", (1, 0): "u", (0, 1): "d", (0, 0): "0"} + return ["".join(mapping[int(row[i]), int(row[n_orbitals + i])] for i in range(n_orbitals)) for row in matrix] + + +def _generate_random_wavefunction( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, +) -> Wavefunction: + """Generate a random normalised Wavefunction for testing.""" + n_alpha = n_electrons // 2 + n_beta = n_electrons - n_alpha + rng = np.random.default_rng(seed) + hf = _hf_determinant(n_alpha, n_beta, n_orbitals) + + seen: set[bytes] = {hf.tobytes()} + dets = [hf] + for _ in range(n_dets * 200): + if len(dets) >= n_dets: + break + exc = _random_excitation(hf, n_orbitals, rng) + if exc is not None and exc.tobytes() not in seen: + seen.add(exc.tobytes()) + dets.append(exc) + + det_matrix = np.array(dets, dtype=np.int8) + configs = [Configuration(s) for s in _determinants_to_configs(det_matrix, n_orbitals)] + + coeff_rng = np.random.default_rng(seed) + raw = coeff_rng.standard_normal(n_dets) + coeffs = raw / np.linalg.norm(raw) + + orbitals = create_test_orbitals(n_orbitals) + return Wavefunction(CasWavefunctionContainer(coeffs, configs, orbitals)) + + +def _matrix_qubit_counts(wf: Wavefunction) -> tuple[int, int]: + """Derive qubit counts from the determinant matrix. + + Returns: + ``(n_system, n_ancilla)`` where + + - *n_system* is the number of system qubits + - *n_ancilla* is the number of ancilla qubits. + + """ + num_orbitals = len(wf.get_orbitals().get_active_space_indices()[0]) + dets = wf.get_active_determinants() + bitstrings = [] + for det in dets: + alpha_str, beta_str = det.to_binary_strings(num_orbitals) + bitstrings.append(beta_str[::-1] + alpha_str[::-1]) + + n_system = len(bitstrings[0]) + matrix = np.array([[int(b) for b in bs] for bs in bitstrings], dtype=np.int8).T + gf2x_result = gf2x_with_tracking(matrix, skip_diagonal_reduction=True, staircase_mode=True) + + synthesizer = BinaryEncodingSynthesizer.from_matrix(gf2x_result.reduced_matrix) + ops = synthesizer.to_gf2x_operations( + num_local_qubits=n_system, + active_qubit_indices=gf2x_result.row_map, + ancilla_start=n_system, + ) + max_select_ancilla = 0 + for op_name, op_args in ops: + if op_name in ("select", "select_and"): + _, address_qubits, _ = op_args + n_addr = len(address_qubits) + max_select_ancilla = max(max_select_ancilla, n_addr - 1) + + return n_system, max_select_ancilla + + +@pytest.fixture +def ozone_wf(test_data_files_path) -> Wavefunction: + """Load the ozone SCI wavefunction from test data.""" + return Wavefunction.from_json_file(str(test_data_files_path / "ozone_sparse_ci_wavefunction.wavefunction.json")) + + +class TestSparseIsometryBinaryEncoding: + """Tests for the sparse isometry binary encoding state preparation.""" + + def test_ozone(self, ozone_wf): + """End-to-end: ozone SCI wavefunction → run() → Circuit → estimate().""" + binary_encoding_prep = create("state_prep", "sparse_isometry_binary_encoding") + circuit = binary_encoding_prep.run(ozone_wf) + assert isinstance(circuit, Circuit) + assert circuit.encoding == "jordan-wigner" + + result = circuit.estimate() + assert isinstance(result, qsharp.estimator.EstimatorResult) + lc = result["logicalCounts"] + assert lc["numQubits"] == 12 # 10 system qubits + 2 ancilla qubits + assert lc["tCount"] == 7 + assert lc["rotationCount"] == 7 + assert lc["cczCount"] == 9 + assert lc["measurementCount"] == 0 + + @pytest.mark.skipif(not QDK_CHEMISTRY_HAS_QISKIT, reason="Qiskit not available") + def test_ozone_statevector(self, ozone_wf): + """Simulate the ozone circuit and verify the statevector matches. + + The circuit may use ancilla qubits beyond the system register. + Ancilla qubits sit on the high-index qubits and are returned + to |0⟩ after uncomputation, so the system-register amplitudes + live in the first 2^n_system entries of the full statevector. + """ + from qiskit.quantum_info import Statevector # noqa: PLC0415 + + from qdk_chemistry.plugins.qiskit.conversion import create_statevector_from_wavefunction # noqa: PLC0415 + + binary_encoding_prep = create("state_prep", "sparse_isometry_binary_encoding") + circuit = binary_encoding_prep.run(ozone_wf) + expected_sv = create_statevector_from_wavefunction(ozone_wf, normalize=True) + n_system = int(np.log2(len(expected_sv))) + + qc = circuit.get_qiskit_circuit() + sim_data = np.array(Statevector.from_instruction(qc)) + + # Extract system-register amplitudes (ancilla qubits should be |0⟩). + system_sv = sim_data[: 2**n_system] + overlap = np.abs(np.vdot(expected_sv, system_sv)) + assert np.isclose( + overlap, 1.0, atol=float_comparison_absolute_tolerance, rtol=float_comparison_relative_tolerance + ) + + @pytest.mark.parametrize( + ("n_electrons", "n_orbitals", "n_dets", "seed"), + [ + (6, 6, 20, 42), + (8, 8, 50, 99), + ], + ids=["6e6o_20det", "8e8o_50det"], + ) + def test_random_wavefunction(self, n_electrons, n_orbitals, n_dets, seed): + """End-to-end: random wavefunction → run() → Circuit → estimate(). + + The expected qubit count is decomposed into system qubits (from the + matrix dimensions) and ancilla qubits (from the compiled Q# circuit). + """ + wf = _generate_random_wavefunction( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=n_dets, + seed=seed, + ) + + binary_encoding_prep = create("state_prep", "sparse_isometry_binary_encoding") + circuit = binary_encoding_prep.run(wf) + assert isinstance(circuit, Circuit) + assert circuit.encoding == "jordan-wigner" + + # Derive qubit counts from the matrix. + # Dense register qubits are system qubits (via rowMap); the extra + # dense_size - 1 qubits are PreparePureStateD's internal scratch. + n_system, n_ancilla = _matrix_qubit_counts(wf) + assert n_system == 2 * n_orbitals + expected_total = n_system + n_ancilla + + # Resource estimate must agree. + lc = circuit.estimate()["logicalCounts"] + assert lc["numQubits"] == expected_total + assert lc["cczCount"] > 0 + + def test_default_settings(self): + """Default settings: include_negative_controls=True, measurement_based_uncompute=False.""" + state_prep = SparseIsometryBinaryEncodingStatePreparation() + assert state_prep.settings().get("include_negative_controls") is True + assert state_prep.settings().get("measurement_based_uncompute") is False + + def test_ozone_negative_controls_disabled(self, ozone_wf): + """Ozone with include_negative_controls=False produces different resource counts.""" + prep = create("state_prep", "sparse_isometry_binary_encoding", include_negative_controls=False) + circuit = prep.run(ozone_wf) + assert isinstance(circuit, Circuit) + lc = circuit.estimate()["logicalCounts"] + assert lc["numQubits"] == 11 + assert lc["tCount"] == 7 + assert lc["rotationCount"] == 7 + assert lc["cczCount"] == 5 + + @pytest.mark.skipif(not QDK_CHEMISTRY_HAS_QISKIT, reason="Qiskit not available") + @pytest.mark.parametrize( + ("n_electrons", "n_orbitals", "n_dets", "seed"), + [ + (6, 6, 20, 42), + (6, 6, 30, 7), + ], + ids=["6e6o_20det", "6e6o_30det"], + ) + def test_random_wavefunction_statevector(self, n_electrons, n_orbitals, n_dets, seed): + """Simulate random-wavefunction circuits and verify the statevector matches.""" + from qiskit.quantum_info import Statevector # noqa: PLC0415 + + from qdk_chemistry.plugins.qiskit.conversion import create_statevector_from_wavefunction # noqa: PLC0415 + + wf = _generate_random_wavefunction( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=n_dets, + seed=seed, + ) + circuit = create("state_prep", "sparse_isometry_binary_encoding").run(wf) + expected_sv = create_statevector_from_wavefunction(wf, normalize=True) + n_system = 2 * n_orbitals + + qc = circuit.get_qiskit_circuit() + sim_data = np.array(Statevector.from_instruction(qc)) + + system_sv = sim_data[: 2**n_system] + overlap = np.abs(np.vdot(expected_sv, system_sv)) + assert np.isclose( + overlap, 1.0, atol=float_comparison_absolute_tolerance, rtol=float_comparison_relative_tolerance + ) + + def test_encode_empty_ops(self): + """Empty ops list produces empty encoded list.""" + assert _encode_gf2x_ops_for_qs([]) == [] + + def test_encode_cx(self): + """CX op is encoded as MatrixCompressionOp('CX', ...).""" + encoded = _encode_gf2x_ops_for_qs([("cx", (0, 1))]) + assert len(encoded) == 1 + assert encoded[0].name == "CX" + assert encoded[0].qubits == [0, 1] + + def test_encode_x(self): + """X op is encoded as MatrixCompressionOp('X', [qubit]).""" + encoded = _encode_gf2x_ops_for_qs([("x", 5)]) + assert encoded[0].name == "X" + assert encoded[0].qubits == [5] + + def test_encode_swap(self): + """SWAP op is encoded correctly.""" + encoded = _encode_gf2x_ops_for_qs([("swap", (2, 3))]) + assert encoded[0].name == "SWAP" + assert encoded[0].qubits == [2, 3] + + def test_encode_ccx(self): + """CCX op encodes [ctrl1, ctrl2, target].""" + encoded = _encode_gf2x_ops_for_qs([("ccx", (4, 0, 1))]) + assert encoded[0].name == "CCX" + assert encoded[0].qubits == [0, 1, 4] + + def test_encode_select(self): + """SELECT op preserves data table and records address qubit count.""" + data = [[True, False], [False, True]] + encoded = _encode_gf2x_ops_for_qs([("select", (data, [0, 1], [2, 3]))]) + assert encoded[0].name == "SELECT" + assert encoded[0].qubits == [0, 1, 2, 3] + assert encoded[0].control_state == 2 + assert encoded[0].lookup_data is data + + def test_encode_select_and(self): + """SELECT_AND is used for measurement-based ops.""" + encoded = _encode_gf2x_ops_for_qs([("select_and", ([[True]], [0], [1]))]) + assert encoded[0].name == "SELECT_AND" + + def test_encode_reversed_order(self): + """Encoded ops appear in reversed order relative to input.""" + encoded = _encode_gf2x_ops_for_qs([("x", 0), ("cx", (1, 2)), ("x", 3)]) + assert [op.name for op in encoded] == ["X", "CX", "X"] + assert encoded[0].qubits == [3] + assert encoded[2].qubits == [0] + + def test_encode_to_dict_keys(self): + """to_dict produces camelCase keys required by Q# bridge.""" + encoded = _encode_gf2x_ops_for_qs([("cx", (0, 1))]) + assert set(encoded[0].to_dict().keys()) == {"name", "qubits", "controlState", "lookupData"} diff --git a/python/tests/test_utils_binary_encoding.py b/python/tests/test_utils_binary_encoding.py new file mode 100644 index 000000000..84e51f8c3 --- /dev/null +++ b/python/tests/test_utils_binary_encoding.py @@ -0,0 +1,683 @@ +"""Tests for the binary encoding utils.""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import numpy as np +import pytest + +from qdk_chemistry.utils.binary_encoding import ( + BinaryEncodingSynthesizer, + MatrixCompressionType, + NotRrefError, + RrefTableau, + _bits_to_int, + _check_rref, + _dense_qubits_size, + _int_to_bits, + _is_diagonal_reduction_shape, + _lookup_select, +) + + +class TestDenseQubitsSize: + """Tests for _dense_qubits_size.""" + + @pytest.mark.parametrize( + ("num_cols", "expected"), + [ + (1, 1), + (2, 1), + (3, 2), + (4, 2), + (5, 3), + (8, 3), + (16, 4), + (1000, 10), + ], + ) + def test_dense_qubits_size(self, num_cols, expected): + """ceil(log2(num_cols)) must equal the expected dense register width.""" + assert _dense_qubits_size(num_cols) == expected + + +class TestIntToBits: + """Tests for _int_to_bits.""" + + @pytest.mark.parametrize( + ("val", "nbits", "expected"), + [ + (0, 4, [False, False, False, False]), + (1, 4, [False, False, False, True]), + (15, 4, [True, True, True, True]), + (5, 3, [True, False, True]), + (0, 1, [False]), + (1, 1, [True]), + (3, 5, [False, False, False, True, True]), + ], + ) + def test_int_to_bits(self, val, nbits, expected): + """Integer must convert to the expected big-endian bit list.""" + assert _int_to_bits(val, nbits) == expected + + +class TestBitsToInt: + """Tests for _bits_to_int.""" + + @pytest.mark.parametrize( + ("bits", "expected"), + [ + ([0, 0, 0], 0), + ([1, 1, 1, 1], 15), + ([1, 0, 1], 5), + ([1], 1), + ([0], 0), + ([True, False, True], 5), + ], + ) + def test_bits_to_int(self, bits, expected): + """Big-endian bit list must convert to the expected integer.""" + assert _bits_to_int(bits) == expected + + def test_roundtrip(self): + """Int -> bits -> int must be the identity for all 4-bit values.""" + for val in range(16): + assert _bits_to_int(_int_to_bits(val, 4)) == val + + +class TestCheckRref: + """Tests for _check_rref RREF validation.""" + + def test_identity_is_rref(self): + """Identity matrix is a valid RREF.""" + _check_rref(np.eye(3, dtype=np.int8)) + + def test_valid_rref_non_square(self): + """Non-square matrix with trailing zero row is valid RREF.""" + mat = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 0, 0]], dtype=np.int8) + _check_rref(mat) + + def test_valid_rref_with_trailing_zeros(self): + """RREF with a trailing all-zero row is accepted.""" + mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) + _check_rref(mat) + + def test_empty_matrix(self): + """All-zero matrix is trivially in RREF.""" + _check_rref(np.zeros((3, 3), dtype=np.int8)) + + def test_non_rref_pivots_not_increasing(self): + """Pivots must appear in strictly increasing column order.""" + mat = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]], dtype=np.int8) + with pytest.raises(NotRrefError, match="not strictly to the right"): + _check_rref(mat) + + def test_non_rref_pivot_col_not_unique(self): + """Pivot column with two non-zero entries is rejected.""" + mat = np.array([[1, 0], [1, 1]], dtype=np.int8) + with pytest.raises(NotRrefError, match="non-zero entries"): + _check_rref(mat) + + def test_non_rref_nonzero_after_zero_row(self): + """Non-zero row appearing after an all-zero row is rejected.""" + mat = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 1]], dtype=np.int8) + with pytest.raises(NotRrefError, match="after an all-zero row"): + _check_rref(mat) + + +class TestIsDiagonalReductionShape: + """Tests for _is_diagonal_reduction_shape.""" + + def test_identity_is_not_staircase(self): + """Identity matrix has no upper-triangular fill, so it is not staircase.""" + assert not _is_diagonal_reduction_shape(np.eye(3, dtype=np.int8)) + + def test_upper_triangular_ones(self): + """Upper-triangular matrix with fill above the diagonal is staircase.""" + mat = np.array([[1, 1, 1], [0, 1, 1], [0, 0, 1]], dtype=np.int8) + assert _is_diagonal_reduction_shape(mat) + + def test_all_zeros(self): + """All-zero matrix is not staircase-shaped.""" + assert not _is_diagonal_reduction_shape(np.zeros((3, 3), dtype=np.int8)) + + +class TestRrefTableau: + """Tests for RrefTableau construction and gate operations.""" + + def _make_rref(self, n_pivots: int, n_extra_cols: int) -> RrefTableau: + """Build a realistic RREF tableau with fill in non-pivot columns. + + The pivot block is an identity matrix. Non-pivot columns get + alternating 0/1 entries (a common pattern after Gaussian + elimination). + + Args: + n_pivots: Number of pivot columns (and rows with leading 1s). + n_extra_cols: Additional non-pivot columns to add after the pivots. + + Returns: + RrefTableau with the specified shape and pivot structure. + + """ + num_cols = n_pivots + n_extra_cols + dense_size = _dense_qubits_size(num_cols) + num_rows = max(n_pivots, dense_size + 1) + mat = np.zeros((num_rows, num_cols), dtype=np.int8) + mat[:n_pivots, :n_pivots] = np.eye(n_pivots, dtype=np.int8) + for c in range(n_pivots, num_cols): + for r in range(n_pivots): + mat[r, c] = (r + c) % 2 + return RrefTableau(mat) + + def test_construction_from_rref(self): + """Valid RREF matrix produces a tableau with correct dimensions and pivots.""" + t = self._make_rref(3, 2) + assert t.num_rows == 4 + assert t.num_cols == 5 + assert t.dense_size == _dense_qubits_size(5) + assert len(t.pivots) == 3 + + def test_construction_rejects_non_rref(self): + """Non-RREF matrix must raise NotRrefError.""" + mat = np.array([[0, 1], [1, 0]], dtype=np.int8) + with pytest.raises(NotRrefError): + RrefTableau(mat) + + def test_get_and_get_col(self): + """Element access and column extraction return correct values.""" + t = self._make_rref(3, 0) + assert t.get(0, 0) is True + assert t.get(0, 1) is False + col = t.get_col(1) + np.testing.assert_array_equal(col, [0, 1, 0]) + + def test_row_is_zero(self): + """Pivot rows are non-zero; trailing rows below rank are zero.""" + t = self._make_rref(3, 2) + assert not t.row_is_zero(0) + assert not t.row_is_zero(2) + assert t.row_is_zero(t.num_rows - 1) + + def test_cx_operation(self): + """CX XORs the control row into the target row.""" + t = self._make_rref(3, 0) + t.cx(0, 1) + np.testing.assert_array_equal(t.data[1], [1, 1, 0]) + np.testing.assert_array_equal(t.data[0], [1, 0, 0]) + + def test_swap_operation(self): + """SWAP exchanges two rows.""" + t = self._make_rref(3, 0) + t.swap(0, 2) + np.testing.assert_array_equal(t.data[0], [0, 0, 1]) + np.testing.assert_array_equal(t.data[2], [1, 0, 0]) + + def test_x_operation(self): + """X flips every bit in the target row.""" + t = self._make_rref(3, 0) + t.x(0) + np.testing.assert_array_equal(t.data[0], [0, 1, 1]) + + def test_toffoli_both_positive(self): + """Toffoli with both controls positive ANDs the two rows into the target.""" + mat = np.array([[1, 0, 1, 1], [0, 1, 1, 0], [0, 0, 0, 0]], dtype=np.int8) + t = RrefTableau(mat) + t.toffoli(2, (0, True), (1, True)) + np.testing.assert_array_equal(t.data[2], [0, 0, 1, 0]) + + def test_toffoli_negative_control(self): + """Toffoli with a negated control ANDs row0 with ~row1 into the target.""" + mat = np.array([[1, 0, 1, 1], [0, 1, 1, 0], [0, 0, 0, 0]], dtype=np.int8) + t = RrefTableau(mat) + t.toffoli(2, (0, True), (1, False)) + np.testing.assert_array_equal(t.data[2], [1, 0, 0, 1]) + + def test_identify_rref_pivots(self): + """Pivot detection returns (row, col) pairs for each leading 1.""" + mat = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 0, 0]], dtype=np.int8) + t = RrefTableau(mat) + assert t.pivots == [(0, 0), (1, 1)] + + def test_permute_columns(self): + """Column permutation reorders all rows accordingly.""" + t = self._make_rref(3, 0) + t.permute_columns([2, 1, 0]) + np.testing.assert_array_equal(t.data[0], [0, 0, 1]) + np.testing.assert_array_equal(t.data[2], [1, 0, 0]) + + def test_toffoli_pui_fixed_and_rest(self): + """Test PUI initialization and per-branch application.""" + mat = np.array( + [ + [1, 0, 1, 0], + [0, 1, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + dtype=np.int8, + ) + t = RrefTableau(mat) + # Fixed control: row 0 must be 1 + t.toffoli_pui_fixed([(0, True)]) + # _tmp_row should be row0 = [1, 0, 1, 0] + np.testing.assert_array_equal(t._tmp_row, [1, 0, 1, 0]) + + # Apply rest: target offset 0 (row dense_size + 0), control: row 1 True + # mask = _tmp_row & row1 = [1,0,1,0] & [0,1,0,1] = [0,0,0,0] + t.toffoli_pui_rest(0, [(1, True)]) + # Row dense_size+0 should be unchanged (XOR with zeros) + np.testing.assert_array_equal(t.data[t.dense_size], [0, 0, 0, 0]) + + +class TestBinaryEncodingSynthesizerBasic: + """Basic construction and property tests.""" + + def test_from_matrix_identity(self): + """Identity RREF matrix should produce a valid synthesiser.""" + mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + assert synth.dense_size == _dense_qubits_size(4) + assert len(synth.bijection) == 4 + + def test_from_matrix_rejects_non_rref(self): + """Non-RREF input must raise NotRrefError.""" + mat = np.array([[0, 1], [1, 0]], dtype=np.int8) + with pytest.raises(NotRrefError): + BinaryEncodingSynthesizer.from_matrix(mat) + + def test_max_batch_size_power_of_two(self): + """max_batch_size must return a positive power of two.""" + mat = np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + dtype=np.int8, + ) + synth = BinaryEncodingSynthesizer(RrefTableau(mat)) + mbs = synth.max_batch_size() + assert mbs > 0 + assert mbs & (mbs - 1) == 0 # power of two + + def test_measurement_based_uncompute_flag(self): + """measurement_based_uncompute flag is stored on the synthesizer.""" + mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat, measurement_based_uncompute=True) + assert synth.measurement_based_uncompute is True + + def test_include_negative_controls_flag(self): + """include_negative_controls flag is stored on the synthesizer.""" + mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat, include_negative_controls=False) + assert synth.include_negative_controls is False + + def test_include_negative_controls_preserves_bijection_semantics(self): + """Both include_negative_controls settings must produce valid bijections.""" + mat = np.array( + [[1, 0, 0, 0, 1, 1], [0, 1, 0, 0, 0, 1], [0, 0, 1, 0, 1, 0], [0, 0, 0, 1, 1, 1]], + dtype=np.int8, + ) + for inc_neg in (True, False): + synth = BinaryEncodingSynthesizer.from_matrix(mat, include_negative_controls=inc_neg) + assert len(synth.bijection) == 6 + ds = synth.dense_size + for dv, c in synth.bijection: + assert _bits_to_int(synth.tableau.data[:ds, c]) == dv + + +class TestBinaryEncodingSynthesizerBijection: + """End-to-end compression correctness for BinaryEncodingSynthesizer. + + Each parametrized RREF matrix is fed through from_matrix(); the tests + verify that the bijection faithfully represents the compressed output. + """ + + @pytest.fixture( + params=[ + "identity_3x4", + "identity_4x5", + "rref_with_fill", + "wide_rref", + "minimal_3x3", + "staircase_4x5", + "all_pivot_4x5", + "many_non_pivot_5x8", + ] + ) + def rref_matrix(self, request) -> np.ndarray: + """Parametrized RREF matrices covering various shapes.""" + matrices = { + # 3 pivots, 1 non-pivot, 1 trailing zero row + "identity_3x4": np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], + dtype=np.int8, + ), + # 4 pivots, 1 non-pivot + "identity_4x5": np.array( + [[1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 0]], + dtype=np.int8, + ), + # 4 pivots, 2 non-pivot columns with fill + "rref_with_fill": np.array( + [ + [1, 0, 0, 0, 1, 1], + [0, 1, 0, 0, 0, 1], + [0, 0, 1, 0, 1, 0], + [0, 0, 0, 1, 1, 1], + ], + dtype=np.int8, + ), + # 5 pivots, 3 non-pivot columns (wide matrix, dense_size=3) + "wide_rref": np.array( + [ + [1, 0, 0, 0, 0, 1, 1, 0], + [0, 1, 0, 0, 0, 0, 1, 1], + [0, 0, 1, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 0, 1, 1, 1], + [0, 0, 0, 0, 1, 0, 0, 1], + ], + dtype=np.int8, + ), + # Minimal: 2 pivots, 1 non-pivot, 1 trailing zero row + "minimal_3x3": np.array( + [[1, 0, 1], [0, 1, 1], [0, 0, 0]], + dtype=np.int8, + ), + # Upper-staircase (diagonal-reduced) shape + "staircase_4x5": np.array( + [ + [1, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 1, 1, 0], + [0, 0, 0, 1, 0], + ], + dtype=np.int8, + ), + # All pivots, single zero non-pivot column (stage-2 trivial) + "all_pivot_4x5": np.array( + [[1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 0]], + dtype=np.int8, + ), + # 4 pivots, 4 non-pivot columns — exercises batch flushing + "many_non_pivot_5x8": np.array( + [ + [1, 0, 0, 0, 1, 1, 0, 1], + [0, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 0, 1, 0, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.int8, + ), + } + return matrices[request.param] + + def test_bijection_covers_all_columns(self, rref_matrix): + """Every column must appear exactly once in the bijection.""" + synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + cols = [c for _, c in synth.bijection] + assert sorted(cols) == list(range(rref_matrix.shape[1])) + + def test_bijection_dense_labels_unique(self, rref_matrix): + """Dense labels must be unique.""" + synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + dense_vals = [dv for dv, _ in synth.bijection] + assert len(set(dense_vals)) == len(dense_vals) + + def test_bijection_dense_labels_fit_in_register(self, rref_matrix): + """All dense labels must fit in the dense register.""" + synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + max_label = (1 << synth.dense_size) - 1 + for dv, _ in synth.bijection: + assert 0 <= dv <= max_label + + def test_sparse_rows_zeroed_after_synthesis(self, rref_matrix): + """After synthesis, all sparse rows should be all-zero.""" + synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + for row in range(synth.dense_size, synth.tableau.num_rows): + assert synth.tableau.row_is_zero(row) + + def test_dense_register_matches_bijection(self, rref_matrix): + """Reading dense rows of each column must reproduce the bijection label. + + This is the core compression-correctness check: the synthesizer + transforms the original RREF matrix so that the top ``dense_size`` + rows encode a binary label for every column, and that label matches + what the bijection records. + """ + synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + ds = synth.dense_size + for dense_val, col in synth.bijection: + actual = _bits_to_int(synth.tableau.data[:ds, col]) + assert actual == dense_val, f"Column {col}: bijection says {dense_val}, but dense register reads {actual}" + + +class TestBinaryEncodingSynthesizerCircuit: + """Tests on the recorded circuit structure.""" + + def test_circuit_nonempty(self): + """Synthesis must produce at least one circuit operation.""" + mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + assert len(synth.circuit) > 0 + + def test_circuit_op_types_valid(self): + """Every circuit entry must be a MatrixCompressionType variant.""" + mat = np.array( + [[1, 0, 0, 0, 1], [0, 1, 0, 0, 1], [0, 0, 1, 0, 0], [0, 0, 0, 1, 1]], + dtype=np.int8, + ) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + for operation_type, _ in synth.circuit: + assert isinstance(operation_type, MatrixCompressionType) + + def test_stage1_starts_with_cx_or_x(self): + """Stage 1 always begins with CX or X (unary staircase).""" + mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + first_operation_type = synth.circuit[0][0] + assert first_operation_type in (MatrixCompressionType.CX, MatrixCompressionType.X) + + +class TestBinaryEncodingSynthesizerReplay: + """Verify that replaying the circuit on the original matrix produces the final tableau.""" + + def test_replay_matches_final_state(self): + """Manually replaying circuit ops on the original matrix must match final tableau.""" + mat = np.array( + [[1, 0, 0, 1, 1], [0, 1, 0, 1, 0], [0, 0, 1, 0, 1], [0, 0, 0, 0, 0]], + dtype=np.int8, + ) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + + # Reconstruct by creating a fresh tableau and replaying + replay = RrefTableau(mat.copy()) + + # Permute columns the same way the solver did + pivot_cols = [p[1] for p in replay.pivots] + pivot_set = set(pivot_cols) + non_pivot = [c for c in range(replay.num_cols) if c not in pivot_set] + col_perm = pivot_cols + non_pivot + replay.permute_columns(col_perm) + + # Replay all operations + for operation_type, payload in synth.circuit: + if operation_type is MatrixCompressionType.CX: + replay.cx(*payload) + elif operation_type is MatrixCompressionType.SWAP: + replay.swap(*payload) + elif operation_type is MatrixCompressionType.TOFFOLI: + tgt, ctrl_pos, ctrl_row, ctrl_val = payload + replay.toffoli(tgt, (ctrl_pos, True), (ctrl_row, ctrl_val)) + elif operation_type is MatrixCompressionType.X: + replay.x(*payload) + elif operation_type is MatrixCompressionType.PUI_BLOCK: + fixed_controls, rest_entries = payload + replay.toffoli_pui_fixed(fixed_controls) + for off, ctrls in rest_entries: + replay.toffoli_pui_rest(off, ctrls) + + # Undo column permutation + inv_perm = [0] * len(col_perm) + for new_idx, old_idx in enumerate(col_perm): + inv_perm[old_idx] = new_idx + replay.permute_columns(inv_perm) + + # Final state should match + np.testing.assert_array_equal(replay.data, synth.tableau.data) + + +class TestToGf2xOperations: + """Tests for operation export.""" + + def test_returns_ops_and_ancilla_count(self): + """to_gf2x_operations returns an op list.""" + mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + ops = synth.to_gf2x_operations(num_local_qubits=3) + assert isinstance(ops, list) + + def test_op_names_are_strings(self): + """All emitted op names must belong to the known gate vocabulary.""" + mat = np.array([[1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + ops = synth.to_gf2x_operations(num_local_qubits=3) + valid_names = {"cx", "swap", "ccx", "x", "mcx", "select", "select_and"} + for name, _ in ops: + assert name in valid_names, f"Unexpected op name: {name}" + + def test_translate_ops_identity_mapping(self): + """When active_qubit_indices is identity, ops stay the same.""" + mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) + synth = BinaryEncodingSynthesizer.from_matrix(mat) + ops_raw = synth.to_gf2x_operations(num_local_qubits=3) + ops_xlat = synth.to_gf2x_operations( + num_local_qubits=3, + active_qubit_indices=[0, 1, 2], + ancilla_start=3, + ) + # With identity mapping and ancilla_start = num_local, should be equivalent + assert len(ops_raw) == len(ops_xlat) + + def test_translate_ops_remaps_indices(self): + """Translation must remap qubit indices through the provided map.""" + ops = [("cx", (0, 1)), ("x", 2)] + translated = BinaryEncodingSynthesizer._translate_ops( + ops, + num_local_qubits=3, + active_qubit_indices=[10, 20, 30], + ancilla_start=100, + ) + assert translated[0] == ("cx", (10, 20)) + assert translated[1] == ("x", 30) + + def test_translate_ops_remaps_ancilla(self): + """Indices >= num_local_qubits should map to ancilla space.""" + ops = [("cx", (0, 3))] + translated = BinaryEncodingSynthesizer._translate_ops( + ops, + num_local_qubits=3, + active_qubit_indices=[10, 20, 30], + ancilla_start=100, + ) + # Index 3 >= num_local_qubits=3, so maps to ancilla_start + (3 - 3) = 100 + assert translated[0] == ("cx", (10, 100)) + + def test_translate_ops_ccx(self): + """CCX indices are remapped through active_qubit_indices.""" + ops = [("ccx", (0, 1, 2))] + translated = BinaryEncodingSynthesizer._translate_ops( + ops, + num_local_qubits=3, + active_qubit_indices=[5, 6, 7], + ancilla_start=10, + ) + assert translated[0] == ("ccx", (5, 6, 7)) + + def test_translate_ops_select(self): + """Select ops remap address and data qubit indices, keeping the table.""" + data_table = [[True, False], [False, True]] + ops = [("select", (data_table, [0, 1], [2, 3]))] + translated = BinaryEncodingSynthesizer._translate_ops( + ops, + num_local_qubits=4, + active_qubit_indices=[10, 11, 12, 13], + ancilla_start=100, + ) + _, (dt, addr, dat) = translated[0] + assert dt is data_table + assert addr == [10, 11] + assert dat == [12, 13] + + def test_translate_ops_mcx(self): + """MCX remaps control and target indices, preserving control states.""" + ops = [("mcx", ([0, 1], [True, False], 2))] + translated = BinaryEncodingSynthesizer._translate_ops( + ops, + num_local_qubits=3, + active_qubit_indices=[5, 6, 7], + ancilla_start=10, + ) + _, (ctrls, state, tgt) = translated[0] + assert ctrls == [5, 6] + assert state == [True, False] + assert tgt == 7 + + def test_measurement_based_uses_select_and(self): + """With measurement_based_uncompute, PUI blocks should emit select_and.""" + mat = np.array( + [[1, 0, 0, 0, 1, 1], [0, 1, 0, 0, 0, 1], [0, 0, 1, 0, 1, 0], [0, 0, 0, 1, 1, 1]], + dtype=np.int8, + ) + synth = BinaryEncodingSynthesizer.from_matrix(mat, measurement_based_uncompute=True) + ops = synth.to_gf2x_operations(num_local_qubits=4) + select_names = {name for name, _ in ops if "select" in name} + if select_names: + assert "select_and" in select_names + + +class TestLookupSelect: + """Tests for the sparse-to-dense lookup table synthesiser.""" + + def test_empty_table(self): + """Empty truth table produces no ops.""" + ops = _lookup_select({}, [0], [1]) + assert ops == [] + + def test_single_entry(self): + """Single-entry table emits one select op.""" + table = {(1,): (1,)} + ops = _lookup_select(table, [0], [1]) + assert len(ops) == 1 + assert ops[0][0] == "select" + + def test_two_address_bits(self): + """Two address bits produce a 2^2 = 4 entry dense data table.""" + table = {(0, 1): (1,), (1, 0): (1,)} + ops = _lookup_select(table, [0, 1], [2]) + assert len(ops) == 1 + name, (data_table, addr, dat) = ops[0] + assert name == "select" + assert addr == [0, 1] + assert dat == [2] + assert len(data_table) == 4 + + def test_data_table_correctness(self): + """Verify the dense Bool[][] table encodes the sparse dict correctly.""" + # Address (1,0) → data (1,0), address (0,1) → data (0,1) + table = {(1, 0): (1, 0), (0, 1): (0, 1)} + ops = _lookup_select(table, [0, 1], [2, 3]) + _, (data_table, _, _) = ops[0] + # addr_int for (1,0): bit0=1, bit1=0 → addr_int=1 + assert data_table[1] == [True, False] + # addr_int for (0,1): bit0=0, bit1=1 → addr_int=2 + assert data_table[2] == [False, True] + # Other entries should be all-false + assert data_table[0] == [False, False] + assert data_table[3] == [False, False] + + def test_select_and_mode(self): + """use_measurement_and=True emits select_and instead of select.""" + table = {(1,): (1,)} + ops = _lookup_select(table, [0], [1], use_measurement_and=True) + assert ops[0][0] == "select_and" From 8db8cdd2e4b7c3f81c0d055e817d609eaa2f4e56 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Mon, 6 Apr 2026 20:41:08 +0000 Subject: [PATCH 03/21] add notebook --- examples/data/ozone.structure.xyz | 7 + examples/random_wavefunction.py | 482 ----------- examples/state_prep_energy.ipynb | 790 ++++-------------- examples/state_prep_sparse_isometry.ipynb | 204 +++++ examples/utils/state_prep_utils.py | 202 +++++ .../test_state_preparation_binary_encoding.py | 9 +- 6 files changed, 560 insertions(+), 1134 deletions(-) create mode 100644 examples/data/ozone.structure.xyz delete mode 100644 examples/random_wavefunction.py create mode 100644 examples/state_prep_sparse_isometry.ipynb create mode 100644 examples/utils/state_prep_utils.py diff --git a/examples/data/ozone.structure.xyz b/examples/data/ozone.structure.xyz new file mode 100644 index 000000000..07078d65e --- /dev/null +++ b/examples/data/ozone.structure.xyz @@ -0,0 +1,7 @@ +3 +Coordinates from ORCA-job singlet_O3 E -225.342972028963 + O 1.23739606886123 -0.26557802176941 0.00000000000000 + O 0.02818212873195 0.02818144808773 0.00000000000000 + O -0.26557819759318 1.23739657368168 0.00000000000000 + + diff --git a/examples/random_wavefunction.py b/examples/random_wavefunction.py deleted file mode 100644 index 74cd5172b..000000000 --- a/examples/random_wavefunction.py +++ /dev/null @@ -1,482 +0,0 @@ -"""Random wavefunction generation for benchmarking state preparation. - -This module generates physically motivated random wavefunctions by -exciting electrons from a Hartree-Fock reference determinant. It is -intended for benchmarking sparse-isometry and binary-encoding circuits -where the full qdk-chemistry pipeline is not needed. - -Public API ----------- -generate_determinants_matrix - Generate a ``(n_dets, 2*n_orbitals)`` matrix of random Slater determinants. -generate_sparse_isometry_matrix - Generate a ``(2*n_orbitals, n_dets)`` binary matrix in the format expected - by the sparse isometry state preparation routines. -generate_sparse_isometry_matrices - Same as above, for multiple determinant counts from a single pool. -generate_random_wavefunction - Generate a :class:`~qdk_chemistry.data.Wavefunction` from random excitations. -determinants_to_config_strings - Convert a determinant matrix to configuration strings. -""" - -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- -from typing import Optional - -import numpy as np -from qdk_chemistry.data import ( - BasisSet, - CasWavefunctionContainer, - Configuration, - Orbitals, - OrbitalType, - Shell, - Wavefunction, -) - -# --------------------------------------------------------------------------- -# Determinant generation -# --------------------------------------------------------------------------- - - -def _hf_determinant(n_alpha: int, n_beta: int, n_orbitals: int) -> np.ndarray: - """Build the Hartree-Fock reference determinant. - - Layout: [alpha_0, alpha_1, ..., alpha_{n_orbitals-1}, - beta_0, beta_1, ..., beta_{n_orbitals-1}] - - The HF state fills the lowest orbitals for each spin channel. - """ - det = np.zeros(2 * n_orbitals, dtype=np.int8) - det[:n_alpha] = 1 # alpha electrons in lowest orbitals - det[n_orbitals : n_orbitals + n_beta] = 1 # beta electrons in lowest orbitals - return det - - -def _random_excitation( - det: np.ndarray, - n_orbitals: int, - rng: np.random.Generator, - max_excitation_order: Optional[int] = None, -) -> Optional[np.ndarray]: - """Generate a single random excitation from a reference determinant. - - Independently applies excitations in the alpha and beta spin channels. - The excitation order is randomly chosen between 1 and max_excitation_order. - - Returns None if the excitation produces a duplicate of the input. - """ - new_det = det.copy() - - # Process alpha and beta channels independently - for channel_start in (0, n_orbitals): - channel = det[channel_start : channel_start + n_orbitals] - occupied = np.where(channel == 1)[0] - virtual = np.where(channel == 0)[0] - - if len(occupied) == 0 or len(virtual) == 0: - continue - - max_exc = min(len(occupied), len(virtual)) - if max_excitation_order is not None: - max_exc = min(max_exc, max_excitation_order) - - # Randomly choose excitation order for this channel (0 means no excitation) - order = rng.integers(0, max_exc + 1) - if order == 0: - continue - - occ_indices = rng.choice(occupied, size=order, replace=False) - vir_indices = rng.choice(virtual, size=order, replace=False) - - new_det[channel_start + occ_indices] = 0 - new_det[channel_start + vir_indices] = 1 - - if np.array_equal(new_det, det): - return None - - return new_det - - -def generate_determinants_matrix( - n_electrons: int, - n_orbitals: int, - n_dets: int, - seed: int = 0, - max_excitation_order: Optional[int] = None, - n_alpha: Optional[int] = None, - n_beta: Optional[int] = None, - include_hf: bool = True, -) -> np.ndarray: - """Generate a matrix of Slater determinants by random excitation from HF. - - Each row is a determinant represented as a binary occupation vector over - 2*n_orbitals spin-orbitals (alpha block followed by beta block). - - This function does NOT enumerate the full determinant space; instead it - randomly samples excitations, making it suitable for large systems where - C(n_orbitals, n_electrons) is intractable. - - Args: - n_electrons: Total number of electrons. - n_orbitals: Number of spatial orbitals (spin-orbitals = 2 * n_orbitals). - n_dets: Number of determinants to generate (including HF if - ``include_hf`` is True). - seed: Random seed for reproducibility. - max_excitation_order: Maximum excitation rank per spin channel. - None means up to the maximum possible. - n_alpha: Number of alpha electrons. Defaults to ``n_electrons // 2``. - n_beta: Number of beta electrons. Defaults to - ``n_electrons - n_alpha``. - include_hf: Whether to always include the HF determinant as the first - row. - - Returns: - np.ndarray of shape ``(n_dets, 2 * n_orbitals)`` with entries 0 or 1. - - Raises: - ValueError: If inputs are inconsistent. - """ - if n_alpha is None: - n_alpha = n_electrons // 2 - if n_beta is None: - n_beta = n_electrons - n_alpha - - if n_alpha + n_beta != n_electrons: - raise ValueError( - f"n_alpha ({n_alpha}) + n_beta ({n_beta}) != n_electrons ({n_electrons})" - ) - if n_alpha > n_orbitals or n_beta > n_orbitals: - raise ValueError( - f"Cannot place {n_alpha} alpha or {n_beta} beta electrons " - f"in {n_orbitals} orbitals" - ) - if n_dets < 1: - raise ValueError("n_dets must be at least 1") - - from math import comb - - max_possible = comb(n_orbitals, n_alpha) * comb(n_orbitals, n_beta) - if n_dets > max_possible: - raise ValueError( - f"Requested {n_dets} determinants but the total space has only " - f"{max_possible}" - ) - - rng = np.random.default_rng(seed) - hf = _hf_determinant(n_alpha, n_beta, n_orbitals) - - seen: set[bytes] = set() - dets: list[np.ndarray] = [] - - if include_hf: - dets.append(hf) - seen.add(hf.tobytes()) - - max_attempts = n_dets * 200 - attempts = 0 - - while len(dets) < n_dets and attempts < max_attempts: - attempts += 1 - new_det = _random_excitation(hf, n_orbitals, rng, max_excitation_order) - if new_det is None: - continue - key = new_det.tobytes() - if key not in seen: - seen.add(key) - dets.append(new_det) - - if len(dets) < n_dets: - raise RuntimeError( - f"Only generated {len(dets)}/{n_dets} unique determinants after " - f"{max_attempts} attempts. Try increasing max_excitation_order or " - f"reducing n_dets." - ) - - return np.array(dets, dtype=np.int8) - - -# --------------------------------------------------------------------------- -# Sparse isometry matrix generation -# --------------------------------------------------------------------------- - - -def _bk_transformation_matrix(n: int) -> np.ndarray: - """Build the n x n Bravyi-Kitaev transformation matrix. - - The BK transformation converts an occupation-number vector f (JW basis) - to a BK qubit vector b via b = B @ f (mod 2). - - The matrix is built recursively: - - B_1 = [[1]] - B_{2k} = [[B_k, 0 ], - [S_k, B_k]] - - where S_k has its last row all 1s (rest zeros). For non-power-of-2 - sizes we pad to the next power of 2 and truncate. - """ - n_padded = 1 - while n_padded < n: - n_padded *= 2 - - def _build(size: int) -> np.ndarray: - if size == 1: - return np.array([[1]], dtype=np.int8) - half = size // 2 - b_half = _build(half) - top = np.hstack([b_half, np.zeros((half, half), dtype=np.int8)]) - s_k = np.zeros((half, half), dtype=np.int8) - s_k[half - 1, :] = 1 - bottom = np.hstack([s_k, b_half]) - return np.vstack([top, bottom]) - - full = _build(n_padded) - return full[:n, :n] - - -def generate_sparse_isometry_matrix( - n_electrons: int, - n_orbitals: int, - n_dets: int, - seed: int = 0, - max_excitation_order: Optional[int] = None, - n_alpha: Optional[int] = None, - n_beta: Optional[int] = None, - include_hf: bool = True, - encoding: str = "jordan-wigner", -) -> np.ndarray: - """Generate a binary matrix in the sparse isometry format. - - Output shape is ``(2 * n_orbitals, n_dets)`` where each column is a - determinant bitstring and each row is a qubit (spin-orbital). - - Args: - n_electrons: Total number of electrons. - n_orbitals: Number of spatial orbitals. - n_dets: Number of determinants to generate. - seed: Random seed for reproducibility. - max_excitation_order: Maximum excitation rank per spin channel. - n_alpha: Number of alpha electrons. - n_beta: Number of beta electrons. - include_hf: Whether to include the HF determinant as the first column. - encoding: ``"jordan-wigner"`` or ``"bravyi-kitaev"``. - - Returns: - np.ndarray of shape ``(2 * n_orbitals, n_dets)`` with entries 0 or 1. - """ - if encoding not in ("jordan-wigner", "bravyi-kitaev"): - raise ValueError( - f"Unknown encoding '{encoding}'. " - "Supported: 'jordan-wigner', 'bravyi-kitaev'" - ) - - det_matrix = generate_determinants_matrix( - n_electrons=n_electrons, - n_orbitals=n_orbitals, - n_dets=n_dets, - seed=seed, - max_excitation_order=max_excitation_order, - n_alpha=n_alpha, - n_beta=n_beta, - include_hf=include_hf, - ) - - # det_matrix is (n_dets, 2*n_orbitals) with [alpha|beta] layout. - # Qubit order q[0]..q[2N-1] = alpha_0 .. alpha_{N-1}, beta_0 .. beta_{N-1} - # which matches det_matrix columns — just transpose. - matrix = det_matrix.T.astype(np.int8) - - if encoding == "bravyi-kitaev": - bk = _bk_transformation_matrix(2 * n_orbitals) - matrix = np.mod(bk @ matrix, 2).astype(np.int8) - - return matrix - - -def generate_sparse_isometry_matrices( - n_electrons: int, - n_orbitals: int, - det_counts: list[int], - seed: int = 0, - max_excitation_order: Optional[int] = None, - n_alpha: Optional[int] = None, - n_beta: Optional[int] = None, - include_hf: bool = True, - encoding: str = "jordan-wigner", -) -> dict[int, np.ndarray]: - """Generate sparse isometry matrices for multiple determinant counts. - - Generates the pool once for ``max(det_counts)`` and slices prefixes. - - Returns: - Dict mapping each count to an ``(2*n_orbitals, count)`` array. - """ - max_dets = max(det_counts) - full_matrix = generate_sparse_isometry_matrix( - n_electrons=n_electrons, - n_orbitals=n_orbitals, - n_dets=max_dets, - seed=seed, - max_excitation_order=max_excitation_order, - n_alpha=n_alpha, - n_beta=n_beta, - include_hf=include_hf, - encoding=encoding, - ) - return {k: full_matrix[:, :k].copy() for k in sorted(det_counts)} - - -# --------------------------------------------------------------------------- -# Configuration string helpers -# --------------------------------------------------------------------------- - - -def determinants_to_config_strings(matrix: np.ndarray, n_orbitals: int) -> list[str]: - """Convert a determinant matrix to configuration strings. - - Convention: '2' = doubly occupied, 'u' = alpha only, - 'd' = beta only, '0' = empty. - - Args: - matrix: Shape ``(n_dets, 2 * n_orbitals)`` binary matrix. - n_orbitals: Number of spatial orbitals. - - Returns: - List of configuration strings, one per determinant. - """ - configs = [] - for det in matrix: - alpha = det[:n_orbitals] - beta = det[n_orbitals:] - chars = [] - for a, b in zip(alpha, beta): - if a == 1 and b == 1: - chars.append("2") - elif a == 1 and b == 0: - chars.append("u") - elif a == 0 and b == 1: - chars.append("d") - else: - chars.append("0") - configs.append("".join(chars)) - return configs - - -# --------------------------------------------------------------------------- -# Wavefunction generation -# --------------------------------------------------------------------------- - - -def create_test_basis_set(num_atomic_orbitals, name="test-basis", structure=None): - """Create a test basis set with the specified number of atomic orbitals. - - Args: - num_atomic_orbitals: Number of atomic orbitals to generate - name: Name for the basis set - structure: a structure to attach - - Returns: - qdk_chemistry.data.BasisSet: A valid basis set for testing - - """ - shells = [] - atom_index = 0 - functions_created = 0 - - # Create shells to reach the desired number of atomic orbitals - while functions_created < num_atomic_orbitals: - remaining = num_atomic_orbitals - functions_created - - if remaining >= 3: - # Add a P shell (3 functions: Px, Py, Pz) - exps = np.array([1.0, 0.5]) - coefs = np.array([0.6, 0.4]) - shell = Shell(atom_index, OrbitalType.P, exps, coefs) - shells.append(shell) - functions_created += 3 - elif remaining >= 1: - # Add S shells for remaining functions (1 function each) - for _ in range(remaining): - exps = np.array([1.0]) - coefs = np.array([1.0]) - shell = Shell(atom_index, OrbitalType.S, exps, coefs) - shells.append(shell) - functions_created += 1 - if structure is not None: - return BasisSet(name, shells, structure) - return BasisSet(name, shells) - - -def create_test_orbitals(num_orbitals: int): - """Helper: construct Orbitals immutably with identity coeffs and occupations. - - Occupations are set in restricted form as total occupancy per MO (0/1/2). - """ - coeffs = np.eye(num_orbitals) - basis_set = create_test_basis_set(num_orbitals) - return Orbitals(coeffs, None, None, basis_set) - - -def generate_random_wavefunction( - n_electrons: int, - n_orbitals: int, - n_dets: int, - seed: int = 0, - max_excitation_order: Optional[int] = None, - n_alpha: Optional[int] = None, - n_beta: Optional[int] = None, - include_hf: bool = True, -) -> "Wavefunction": - """Generate a random :class:`~qdk_chemistry.data.Wavefunction`. - - Follows the same construction pattern as the ``wavefunction_4e4o`` - test fixture in ``conftest.py``: - - 1. Generate determinants via random excitation from HF. - 2. Convert to :class:`~qdk_chemistry.data.Configuration` strings. - 3. Assign random normalised coefficients. - 4. Wrap in ``CasWavefunctionContainer`` → ``Wavefunction``. - - Args: - n_electrons: Total number of electrons. - n_orbitals: Number of spatial orbitals. - n_dets: Number of determinants. - seed: Random seed for reproducibility. - max_excitation_order: Maximum excitation rank per spin channel. - n_alpha: Number of alpha electrons. - n_beta: Number of beta electrons. - include_hf: Whether to include the HF determinant. - - Returns: - A normalised :class:`~qdk_chemistry.data.Wavefunction`. - """ - - det_matrix = generate_determinants_matrix( - n_electrons=n_electrons, - n_orbitals=n_orbitals, - n_dets=n_dets, - seed=seed, - max_excitation_order=max_excitation_order, - n_alpha=n_alpha, - n_beta=n_beta, - include_hf=include_hf, - ) - - config_strings = determinants_to_config_strings(det_matrix, n_orbitals) - dets = [Configuration(s) for s in config_strings] - - # Random normalised coefficients (offset seed to avoid correlation - # with determinant generation) - rng = np.random.default_rng(seed + 999) - raw = rng.standard_normal(n_dets) - coeffs = raw / np.linalg.norm(raw) - - # Identity MO coefficients — sufficient for state-preparation benchmarks - orbitals = create_test_orbitals(n_orbitals) - - container = CasWavefunctionContainer(coeffs, dets, orbitals) - return Wavefunction(container) diff --git a/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index 4b6fa7d4c..83057d76a 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -1,29 +1,76 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "115ee185", + "metadata": {}, + "source": [ + "# Using `qdk-chemistry` for multi-reference quantum chemistry state preparation and energy estimation\n", + "\n", + "This notebook demonstrates an end-to-end multi-configurational quantum chemistry workflow using `qdk-chemistry`.\n", + "It covers molecule loading and visualization, self-consistent-field (SCF) calculation, active-space selection, multi-configurational wavefunction generation, quantum state-preparation circuit construction, and measurement circuits for energy estimation.\n", + "\n", + "**Prerequisites:** In addition to [installing `qdk-chemistry`](https://github.com/microsoft/qdk-chemistry/blob/main/INSTALL.md), you will need to install the `jupyter` extra to run this notebook:\n", + "\n", + "```bash\n", + "pip install 'qdk-chemistry[jupyter]'\n", + "```\n", + "\n", + "This installs the additional dependencies required by this notebook (ipykernel, pandas, and pyscf).\n", + "\n", + "---\n", + "\n", + "In many molecular systems—such as bond dissociation or transition-metal complexes—a single electronic configuration cannot describe the true electronic structure.\n", + "These multi-configurational systems exhibit strong electron correlation that challenges mean-field and single-determinant methods like [Hartree–Fock](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method) or standard [coupled cluster theory](https://en.wikipedia.org/wiki/Coupled_cluster).\n", + "\n", + "While classical multi-configurational approaches can capture these effects, their computational cost grows exponentially with system size.\n", + "Quantum computers offer a complementary route: they can represent superpositions of many configurations natively and solve these problems with polynomial scaling.\n", + "\n", + "However, near-term fault-tolerant quantum hardware is still in the early stages of growth and scaling.\n", + "To use it effectively, we must compress and optimize chemistry problems before they reach the quantum device.\n", + "Classical methods enable this by identifying essential orbitals through active-space selection, generating approximate wavefunctions for state preparation, and supplying data to optimize quantum circuits for energy estimation.\n", + "\n", + "This notebook focuses on state preparation, where a multi-configurational wavefunction from classical computation is transformed into a quantum circuit.\n", + "State preparation is central to quantum chemistry algorithms such as [Quantum Phase Estimation (QPE)](https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) and also serves as a practical hardware benchmark: preparing complex multi-configurational states tests the fidelity and coherence of quantum hardware.\n", + "\n", + "In the example below, we show how to generate and optimize state preparation circuits, from active-space selection to energy measurement, demonstrating how chemical insight can reduce quantum resource requirements for near-term devices." + ] + }, + { + "cell_type": "markdown", + "id": "d0a5a02f", + "metadata": {}, + "source": [ + "## Loading and visualizing the molecular structure\n", + "\n", + "For this example, we will use the benzene diradical molecule.\n", + "The benzene diradical has two unpaired electrons, making it a good candidate for multi-reference quantum chemistry methods.\n", + "This molecule is also an important intermediate in the [Bergman cyclization reaction](https://en.wikipedia.org/wiki/Bergman_cyclization), a popular reaction in synthetic organic chemistry.\n", + "\n", + "The molecular structure is provided in the [XYZ file format](https://en.wikipedia.org/wiki/XYZ_file_format).\n", + "This cell demonstrates how to load the molecule and visualize its structure." + ] + }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5436376a", "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n let rules = [\n {\n token: \"comment\",\n regex: /(\\/\\/).*/,\n beginWord: false,\n },\n {\n token: \"string\",\n regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n beginWord: false,\n },\n {\n token: \"keyword\",\n regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(not|and|or)\\b|(w/)`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(=)|(!)|(<)|(>)|(\\+)|(-)|(\\*)|(/)|(\\^)|(%)|(\\|)|(&&&)|(~~~)|(\\.\\.\\.)|(\\.\\.)|(\\?)`,\n beginWord: false,\n },\n {\n token: \"meta\",\n regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n beginWord: true,\n },\n {\n token: \"atom\",\n regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n beginWord: true,\n },\n ];\n let simpleRules = [];\n for (let rule of rules) {\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(rule.regex, \"g\"),\n sol: rule.beginWord,\n });\n if (rule.beginWord) {\n // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n sol: false,\n });\n }\n }\n\n // Register the mode defined above with CodeMirror\n window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n reg: [/^%%qsharp/],\n };\n\n // Force re-highlighting of all cells the first time this code runs\n for (const cell of window.Jupyter.notebook.get_cells()) {\n cell.auto_highlight();\n }\n });\n}\n", - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from pathlib import Path\n", "\n", + "from qdk.widgets import MoleculeViewer\n", + "\n", "from qdk_chemistry.data import Structure\n", "\n", "# Read molecular structure from XYZ file\n", "structure = Structure.from_xyz_file(\n", " Path(\".\") / \"data/benzene_diradical.structure.xyz\"\n", - ")" + ")\n", + "\n", + "# Visualize the molecular structure\n", + "display(MoleculeViewer(molecule_data=structure.to_xyz()))" ] }, { @@ -40,80 +87,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "75d71220", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[2026-04-02 17:15:23.081763] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] mol: atoms=10, electrons=40, n_ecp_electrons=0, charge=0, multiplicity=1, spin(2S)=0, alpha=20, beta=20\n", - "[2026-04-02 17:15:23.081813] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] restricted=true, basis=cc-pvdz, pure=true, num_atomic_orbitals=104, density_threshold=1.00e-05, og_threshold=1.00e-07\n", - "[2026-04-02 17:15:23.081816] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] fock_alg=TradJK\n", - "[2026-04-02 17:15:23.081818] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] eri_threshold=1.00e-12, shell_pair_threshold=1.00e-12\n", - "[2026-04-02 17:15:23.081821] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] world_size=1, omp_get_max_threads=8\n", - "[2026-04-02 17:15:23.238719] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", - "[2026-04-02 17:15:23.487542] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 000: E=-2.322479405965371e+02, DE=-2.322479405965371e+02, |DP|=5.849507414288221e-02, |DG|=2.118472909388689e-02, \n", - "[2026-04-02 17:15:23.563530] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", - "[2026-04-02 17:15:24.075657] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 001: E=-2.291813329598724e+02, DE=+3.066607636664656e+00, |DP|=3.126260151046291e-02, |DG|=7.249889224905371e-04, \n", - "[2026-04-02 17:15:24.156506] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", - "[2026-04-02 17:15:24.603974] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 002: E=-2.292553237397959e+02, DE=-7.399077992351977e-02, |DP|=9.803468311889503e-03, |DG|=3.195504248892452e-04, \n", - "[2026-04-02 17:15:25.115811] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 003: E=-2.292640447846453e+02, DE=-8.721044849323789e-03, |DP|=1.996996960235905e-03, |DG|=7.614580500099797e-05, \n", - "[2026-04-02 17:15:25.589662] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 004: E=-2.292653960205162e+02, DE=-1.351235870913570e-03, |DP|=1.704011448517370e-03, |DG|=1.743802168804288e-05, \n", - "[2026-04-02 17:15:26.072640] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 005: E=-2.292654536894343e+02, DE=-5.766891808889341e-05, |DP|=3.921150071152143e-04, |DG|=8.892118464230874e-06, \n", - "[2026-04-02 17:15:26.072707] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:diis_gdm] Switching from DIIS to GDM at step 6 (delta_energy=-5.7668918088893406e-05, max_diis_step=50)\n", - "[2026-04-02 17:15:26.084450] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:gdm] No history available, using initial Hessian inverse\n", - "[2026-04-02 17:15:27.355966] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 006: E=-2.292654599091643e+02, DE=-6.219730039447313e-06, |DP|=3.590797971823754e-05, |DG|=4.330434592709749e-06, \n", - "[2026-04-02 17:15:28.942314] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 007: E=-2.292654625017283e+02, DE=-2.592563987491303e-06, |DP|=4.415900623359871e-05, |DG|=3.447236387212594e-06, \n", - "[2026-04-02 17:15:30.650865] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 008: E=-2.292654634735950e+02, DE=-9.718667115521384e-07, |DP|=3.507633868007982e-05, |DG|=2.008776644399891e-06, \n", - "[2026-04-02 17:15:31.538733] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 009: E=-2.292654637022202e+02, DE=-2.286252538397093e-07, |DP|=1.629217307319500e-05, |DG|=4.675644360142092e-07, \n", - "[2026-04-02 17:15:32.278392] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 010: E=-2.292654637160156e+02, DE=-1.379532932332950e-08, |DP|=1.732546445848254e-06, |DG|=1.236880060990507e-07, \n", - "[2026-04-02 17:15:33.087694] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 011: E=-2.292654637177791e+02, DE=-1.763510226737708e-09, |DP|=9.451803020885315e-07, |DG|=1.114090723350550e-07, \n", - "[2026-04-02 17:15:34.220548] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf_algorithm:scf_algorithm] Step 012: E=-2.292654637185898e+02, DE=-8.107008397928439e-10, |DP|=8.882188359183630e-07, |DG|=4.954809240302295e-08, \n", - "[2026-04-02 17:15:34.766661] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] Reset incremental Fock matrix\n", - "[2026-04-02 17:15:35.255372] [info] [qdk:chemistry:algorithms:microsoft:scf:src:scf:scf_impl] SCF converged: steps=13, E=-229.265463718590\n", - "-----------------------------------------------------------------\n", - "Nuclear Repulsion Energy = 184.689150054839\n", - "One-Electron Energy = -673.763662885958\n", - "Two-Electron Energy = 259.809049112529\n", - "DFT Exchange-Correlation Energy = 0.000000000000\n", - "Total Energy = -229.265463718590\n", - "\n", - "Total Dipole (a.u.)\n", - " X_ = -0.000000000000\n", - " Y = 0.000000000003\n", - " Z = 0.000000000000\n", - "\n", - "Mulliken Charges (a.u.)\n", - " Atom 0 Z= 6 -9.54308573e-02\n", - " Atom 1 Z= 6 -2.60256781e-02\n", - " Atom 2 Z= 6 -2.60256781e-02\n", - " Atom 3 Z= 6 -9.54308573e-02\n", - " Atom 4 Z= 6 -2.60256781e-02\n", - " Atom 5 Z= 6 -2.60256781e-02\n", - " Atom 6 Z= 1 7.37411067e-02\n", - " Atom 7 Z= 1 7.37411067e-02\n", - " Atom 8 Z= 1 7.37411067e-02\n", - " Atom 9 Z= 1 7.37411067e-02\n", - "-----------------------------------------------------------------\n", - "SCF energy is -229.265 Hartree\n", - "SCF Orbitals:\n", - " Orbitals Summary:\n", - " AOs: 104\n", - " MOs: 104\n", - " Type: Restricted\n", - " Has overlap: Yes\n", - " Has basis set: Yes\n", - " Valid: Yes\n", - " Has active space: Yes\n", - " Active Orbitals: α=104, β=104\n", - " Has inactive space: No\n", - " Virtual Orbitals: α=0, β=0\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "from qdk_chemistry.algorithms import create\n", "\n", @@ -149,33 +126,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "2f5ea942", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active Space Orbitals:\n", - " Orbitals Summary:\n", - " AOs: 104\n", - " MOs: 104\n", - " Type: Restricted\n", - " Has overlap: Yes\n", - " Has basis set: Yes\n", - " Valid: Yes\n", - " Has active space: Yes\n", - " Active Orbitals: α=6, β=6\n", - " Has inactive space: Yes\n", - " Inactive Orbitals: α=17, β=17\n", - " Virtual Orbitals: α=81, β=81\n", - "\n", - "[2026-04-02 17:15:35.349861] [info] [qdk:chemistry:algorithms:microsoft:active_space:valence_active_space] ValenceActiveSpaceSelector::Starting active space selection.\n", - "[2026-04-02 17:15:35.350027] [info] [qdk:chemistry:algorithms:microsoft:active_space:valence_active_space] ValenceActiveSpaceSelector::Selected active space of 6 orbitals: 17, 18, 19, 20, 21, 22\n" - ] - } - ], + "outputs": [], "source": [ "# Select active space (6 electrons in 6 orbitals for benzene diradical) to choose most chemically relevant orbitals\n", "active_space_selector = create(\"active_space_selector\", algorithm_name=\"qdk_valence\",\n", @@ -196,6 +150,28 @@ "The drop-down menu provides the ability to select different occupied and virtual orbitals in the active space to visualize their shapes, while the isovalue slider adjusts the surface representation of the orbitals for different electron density levels.\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d8850fc", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.utils.cubegen import generate_cubefiles_from_orbitals\n", + "\n", + "# Generate cube files for the active orbitals\n", + "cube_data = generate_cubefiles_from_orbitals(\n", + " orbitals=active_orbitals,\n", + " grid_size=(40, 40, 40),\n", + " margin=10.0,\n", + " indices=active_orbitals.get_active_space_indices()[0],\n", + " label_maker=lambda p: f\"{'occupied' if p < 20 else 'virtual'}_{p + 1:04d}\"\n", + ")\n", + "\n", + "# Visualize the molecular orbitals together with the structure\n", + "MoleculeViewer(molecule_data=structure.to_xyz(), cube_data=cube_data)" + ] + }, { "cell_type": "markdown", "id": "f8b8e498", @@ -218,51 +194,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "980dae08", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Active Space Hamiltonian:\n", - " Hamiltonian Summary:\n", - " Type: Hermitian\n", - " Restrictedness: Restricted\n", - " Active Orbitals: 6\n", - " Total Orbitals: 104\n", - " Core Energy: -223.094600\n", - " Integral Statistics:\n", - " One-body Integrals (alpha): 36 (larger than 0.000001: 6)\n", - " Two-body Integrals (aaaa): 1296 (larger than 0.000001: 168)\n", - "\n", - "[2026-04-02 17:15:37.073] [ci_solver] [info] [Selected CI Solver]:\n", - "[2026-04-02 17:15:37.073] [ci_solver] [info] NDETS = 400, MATEL_TOL = 2.22045e-16, RES_TOL = 1.00000e-06, MAX_SUB = 200\n", - "[2026-04-02 17:15:37.084] [ci_solver] [info] NNZ = 22448, H_DUR = 1.05001e+01 ms\n", - "[2026-04-02 17:15:37.084] [ci_solver] [info] HMEM_LOC = 6.54e-04 GiB\n", - "[2026-04-02 17:15:37.084] [ci_solver] [info] H_SPARSE = 1.40e+01%\n", - "[2026-04-02 17:15:37.084] [ci_solver] [info] * Will generate identity guess\n", - "[2026-04-02 17:15:37.084] [davidson] [info] [Davidson Eigensolver]:\n", - "[2026-04-02 17:15:37.084] [davidson] [info] N = 400, MAX_M = 200, RES_TOL = 1.00000e-06\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 1, LAM(0) = -6.272181531114e+00, RNORM = 1.508328251603e-01\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 2, LAM(0) = -6.315584336020e+00, RNORM = 7.639642777831e-02\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 3, LAM(0) = -6.323257851542e+00, RNORM = 2.095312033898e-02\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 4, LAM(0) = -6.323885609955e+00, RNORM = 7.536714097302e-03\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 5, LAM(0) = -6.323957966169e+00, RNORM = 1.432015296461e-03\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 6, LAM(0) = -6.323963692872e+00, RNORM = 7.115396705615e-04\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 7, LAM(0) = -6.323964619315e+00, RNORM = 2.254315103486e-04\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 8, LAM(0) = -6.323964690443e+00, RNORM = 4.475298011079e-05\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 9, LAM(0) = -6.323964694013e+00, RNORM = 1.094187112506e-05\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 10, LAM(0) = -6.323964694331e+00, RNORM = 4.039726192293e-06\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 11, LAM(0) = -6.323964694356e+00, RNORM = 1.067837448925e-06\n", - "[2026-04-02 17:15:37.084] [davidson] [info] iter = 12, LAM(0) = -6.323964694358e+00, RNORM = 2.304002722527e-07\n", - "[2026-04-02 17:15:37.084] [davidson] [info] Davidson Converged!\n", - "[2026-04-02 17:15:37.084] [ci_solver] [info] DAV_NITER = 12, E0 = -6.323965e+00 Eh, DAVIDSON_DUR = 4.61096e-01 ms\n", - "CASCI energy is -229.419 Hartree, and the electron correlation energy is -0.153 Hartree\n" - ] - } - ], + "outputs": [], "source": [ "# Construct Hamiltonian in the active space and print its summary\n", "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", @@ -302,33 +237,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "ebba573c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total determinants in the CASCI wavefunction: 400\n", - "Plotting the top 10 determinants by weight.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9cf9edec687c4db7be9cd809ac440ee3", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import numpy as np\n", "from qdk.widgets import Histogram\n", @@ -338,7 +250,7 @@ "print(f\"Total determinants in the CASCI wavefunction: {len(wfn_cas.get_active_determinants())}\")\n", "print(f\"Plotting the top {NUM_DETERMINANTS} determinants by weight.\")\n", "top_configurations = wfn_cas.get_top_determinants(max_determinants=NUM_DETERMINANTS)\n", - "display(Histogram(bar_values={k.to_string(): np.abs(v)**2 for k, v in top_configurations.items()},))" + "display(Histogram(bar_values={k.to_string(): np.abs(v)**2 for k, v in top_configurations.items()},sort=\"high-to-low\",))" ] }, { @@ -354,54 +266,31 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "560f8554", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Reference energy for top 2 determinants is -229.397913 Hartree\n" - ] - } - ], + "outputs": [], "source": [ - "# Get top 2 determinants from the CASCI wavefunction to form a sparse wavefunction\n", - "top_configurations = wfn_cas.get_top_determinants(max_determinants=8)\n", - "\n", "# Compute the reference energy of the sparse wavefunction\n", "pmc_calculator = create(\"projected_multi_configuration_calculator\")\n", "E_sparse, wfn_sparse = pmc_calculator.run(hamiltonian, list(top_configurations.keys()))\n", "\n", - "print(f\"Reference energy for top 2 determinants is {E_sparse:.6f} Hartree\")" + "print(f\"Reference energy for top {NUM_DETERMINANTS} determinants is {E_sparse:.6f} Hartree\")" ] }, { - "cell_type": "code", - "execution_count": 7, - "id": "b3708467", + "cell_type": "markdown", + "id": "e133afd5", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Determinant: 000111000111, Coefficient: 0.787612\n", - "Determinant: 001011001011, Coefficient: -0.580092\n", - "Determinant: 010110010110, Coefficient: -0.099398\n", - "Determinant: 100101100101, Coefficient: -0.104064\n", - "Determinant: 101001101001, Coefficient: 0.075494\n", - "Determinant: 011010011010, Coefficient: 0.074410\n", - "Determinant: 100011001101, Coefficient: -0.074884\n", - "Determinant: 001101100011, Coefficient: -0.074884\n" - ] - } - ], "source": [ - "for det, coeffs in zip(wfn_sparse.get_active_determinants(), wfn_sparse.get_coefficients()):\n", - " alpha_str, beta_str = det.to_binary_strings(6)\n", - " print(f\"Determinant: {alpha_str[::-1]}{beta_str[::-1]}, Coefficient: {coeffs:.6f}\")" + "### Loading the wavefunction using general state preparation methods\n", + "\n", + "One possibility for loading the multi-configuration wavefunction onto a quantum computer is to use general state preparation approaches such as the [isometry method](https://arxiv.org/abs/1501.06911), as offered in software such as [Qiskit](https://qiskit.org/documentation/stubs/qiskit.circuit.library.Isometry.html).\n", + "While this is a very powerful general-purpose approach, it can be resource intensive, requiring very deep circuits even for modest-sized wavefunctions due to its exponential scaling in the number of qubits.\n", + "This approach also requires numerous fine rotations—operations that can be challenging for near-term fault-tolerant quantum hardware.\n", + "This cell demonstrates how to use the isometry method to generate a quantum circuit for preparing the multi-configuration wavefunction on a quantum computer.\n", + "\n", + "**Note**: the generated circuits are so deep that you will need to adjust the \"zoom\" selection in the visualization window to see the detailed operations." ] }, { @@ -445,112 +334,7 @@ "execution_count": null, "id": "9baaf705", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[2026-04-02 17:15:40.744314] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", - "[2026-04-02 17:15:40.765453] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", - "[2026-04-02 17:15:40.774934] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", - "[2026-04-02 17:15:40.791613] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", - "[2026-04-02 17:15:40.800593] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d5fef22a0d824055914a48b7c10bfafd", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Logical EstimateCounts
0numQubits12
1tCount0
2rotationCount62
3rotationDepth62
4cczCount0
5ccixCount0
6measurementCount0
\n", - "
" - ], - "text/plain": [ - " Logical Estimate Counts\n", - "0 numQubits 12\n", - "1 tCount 0\n", - "2 rotationCount 62\n", - "3 rotationDepth 62\n", - "4 cczCount 0\n", - "5 ccixCount 0\n", - "6 measurementCount 0" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import pandas as pd\n", "from qdk.widgets import Circuit\n", @@ -571,232 +355,59 @@ { "cell_type": "code", "execution_count": null, - "id": "3d942859", + "id": "9c7a1149", "metadata": {}, "outputs": [], "source": [ - "from qdk_chemistry.data import (\n", - " Ansatz,\n", - " BasisSet,\n", - " CanonicalFourCenterHamiltonianContainer,\n", - " CasWavefunctionContainer,\n", - " Configuration,\n", - " Hamiltonian,\n", - " Orbitals,\n", - " OrbitalType,\n", - " Shell,\n", - " Structure,\n", - " Wavefunction,\n", - ")\n", + "import pandas as pd\n", + "from qdk.widgets import Circuit\n", "\n", - "def create_test_basis_set(num_atomic_orbitals, name=\"test-basis\", structure=None):\n", - " \"\"\"Create a test basis set with the specified number of atomic orbitals.\n", - "\n", - " Args:\n", - " num_atomic_orbitals: Number of atomic orbitals to generate\n", - " name: Name for the basis set\n", - " structure: a structure to attach\n", - "\n", - " Returns:\n", - " qdk_chemistry.data.BasisSet: A valid basis set for testing\n", - "\n", - " \"\"\"\n", - " shells = []\n", - " atom_index = 0\n", - " functions_created = 0\n", - "\n", - " # Create shells to reach the desired number of atomic orbitals\n", - " while functions_created < num_atomic_orbitals:\n", - " remaining = num_atomic_orbitals - functions_created\n", - "\n", - " if remaining >= 3:\n", - " # Add a P shell (3 functions: Px, Py, Pz)\n", - " exps = np.array([1.0, 0.5])\n", - " coefs = np.array([0.6, 0.4])\n", - " shell = Shell(atom_index, OrbitalType.P, exps, coefs)\n", - " shells.append(shell)\n", - " functions_created += 3\n", - " elif remaining >= 1:\n", - " # Add S shells for remaining functions (1 function each)\n", - " for _ in range(remaining):\n", - " exps = np.array([1.0])\n", - " coefs = np.array([1.0])\n", - " shell = Shell(atom_index, OrbitalType.S, exps, coefs)\n", - " shells.append(shell)\n", - " functions_created += 1\n", - " if structure is not None:\n", - " return BasisSet(name, shells, structure)\n", - " return BasisSet(name, shells)\n", - "\n", - "\n", - "def create_test_orbitals(num_orbitals: int):\n", - " \"\"\"Helper: construct Orbitals immutably with identity coeffs and occupations.\n", - "\n", - " Occupations are set in restricted form as total occupancy per MO (0/1/2).\n", - " \"\"\"\n", - " coeffs = np.eye(num_orbitals)\n", - " basis_set = create_test_basis_set(num_orbitals)\n", - " return Orbitals(coeffs, None, None, basis_set)\n" + "# Generate state preparation circuit for the sparse state via sparse isometry binary_encoding\n", + "state_prep = create(\"state_prep\", \"sparse_isometry_binary_encoding\")\n", + "sparse_isometry_binary_encoding_circuit = state_prep.run(wfn_sparse)\n", + "\n", + "# Visualize the sparse isometry circuit\n", + "display(Circuit(sparse_isometry_binary_encoding_circuit.get_qsharp_circuit(prune_classical_qubits=True)))\n", + "\n", + "# Print logical qubit counts estimated from the circuit\n", + "df = pd.DataFrame(\n", + " sparse_isometry_binary_encoding_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "display(df)" ] }, { - "cell_type": "code", - "execution_count": 1, - "id": "67663ad2", + "cell_type": "markdown", + "id": "6635d626", + "metadata": {}, + "source": [ + "Rather than requiring thousands of fine rotations, this optimized approach requires only a single fine rotation for the two-determinant benzene diradical wavefunction—demonstrating the power of chemistry-informed optimizations for quantum state preparation.\n", + "\n", + "Close inspection of the generated circuit shows that it has also reduced our qubit count: several of the qubits have been converted to classical bits, which can be post-processed after measurement.\n", + "We will revisit these classical bits in the next section on energy measurement." + ] + }, + { + "cell_type": "markdown", + "id": "4588419b", "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file provides CodeMirror syntax highlighting for Q# magic cells\n// in classic Jupyter Notebooks. It does nothing in other (Jupyter Notebook 7,\n// VS Code, Azure Notebooks, etc.) environments.\n\n// Detect the prerequisites and do nothing if they don't exist.\nif (window.require && window.CodeMirror && window.Jupyter) {\n // The simple mode plugin for CodeMirror is not loaded by default, so require it.\n window.require([\"codemirror/addon/mode/simple\"], function defineMode() {\n let rules = [\n {\n token: \"comment\",\n regex: /(\\/\\/).*/,\n beginWord: false,\n },\n {\n token: \"string\",\n regex: String.raw`^\\\"(?:[^\\\"\\\\]|\\\\[\\s\\S])*(?:\\\"|$)`,\n beginWord: false,\n },\n {\n token: \"keyword\",\n regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled|internal)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\\b`,\n beginWord: true,\n },\n {\n token: \"keyword\",\n regex: String.raw`(let|set|use|borrow|mutable)\\b`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(not|and|or)\\b|(w/)`,\n beginWord: true,\n },\n {\n token: \"operatorKeyword\",\n regex: String.raw`(=)|(!)|(<)|(>)|(\\+)|(-)|(\\*)|(/)|(\\^)|(%)|(\\|)|(&&&)|(~~~)|(\\.\\.\\.)|(\\.\\.)|(\\?)`,\n beginWord: false,\n },\n {\n token: \"meta\",\n regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\\b`,\n beginWord: true,\n },\n {\n token: \"atom\",\n regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\\b`,\n beginWord: true,\n },\n ];\n let simpleRules = [];\n for (let rule of rules) {\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(rule.regex, \"g\"),\n sol: rule.beginWord,\n });\n if (rule.beginWord) {\n // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token\n simpleRules.push({\n token: rule.token,\n regex: new RegExp(String.raw`\\W` + rule.regex, \"g\"),\n sol: false,\n });\n }\n }\n\n // Register the mode defined above with CodeMirror\n window.CodeMirror.defineSimpleMode(\"qsharp\", { start: simpleRules });\n window.CodeMirror.defineMIME(\"text/x-qsharp\", \"qsharp\");\n\n // Tell Jupyter to associate %%qsharp magic cells with the qsharp mode\n window.Jupyter.CodeCell.options_default.highlight_modes[\"qsharp\"] = {\n reg: [/^%%qsharp/],\n };\n\n // Force re-highlighting of all cells the first time this code runs\n for (const cell of window.Jupyter.notebook.get_cells()) {\n cell.auto_highlight();\n }\n });\n}\n", - "text/plain": [] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "ename": "TypeError", - "evalue": "__init__(): incompatible constructor arguments. The following argument types are supported:\n 1. qdk_chemistry._core.data.Orbitals(arg0: qdk_chemistry._core.data.Orbitals)\n 2. qdk_chemistry._core.data.Orbitals(coefficients: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n 3. qdk_chemistry._core.data.Orbitals(coefficients_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], coefficients_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, energies_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n\nInvoked with: array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]), None, None", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m 3\u001b[39m num_qubits = \u001b[32m26\u001b[39m\n\u001b[32m 4\u001b[39m seed = \u001b[32m42\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m wfn = \u001b[43mgenerate_random_wavefunction\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn_electrons\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnum_qubits\u001b[49m\u001b[43m/\u001b[49m\u001b[43m/\u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_orbitals\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnum_qubits\u001b[49m\u001b[43m/\u001b[49m\u001b[43m/\u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_dets\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnum_qubits\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mseed\u001b[49m\u001b[43m=\u001b[49m\u001b[43mseed\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m/workspaces/qdk-chemistry/examples/random_wavefunction.py:422\u001b[39m, in \u001b[36mgenerate_random_wavefunction\u001b[39m\u001b[34m(n_electrons, n_orbitals, n_dets, seed, max_excitation_order, n_alpha, n_beta, include_hf)\u001b[39m\n\u001b[32m 419\u001b[39m coeffs = raw / np.linalg.norm(raw)\n\u001b[32m 421\u001b[39m \u001b[38;5;66;03m# Identity MO coefficients — sufficient for state-preparation benchmarks\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m422\u001b[39m orbitals = \u001b[43mOrbitals\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43meye\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn_orbitals\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 424\u001b[39m container = CasWavefunctionContainer(coeffs, dets, orbitals)\n\u001b[32m 425\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m Wavefunction(container)\n", - "\u001b[31mTypeError\u001b[39m: __init__(): incompatible constructor arguments. The following argument types are supported:\n 1. qdk_chemistry._core.data.Orbitals(arg0: qdk_chemistry._core.data.Orbitals)\n 2. qdk_chemistry._core.data.Orbitals(coefficients: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n 3. qdk_chemistry._core.data.Orbitals(coefficients_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], coefficients_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"], energies_alpha: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, energies_beta: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, 1]\"] | None = None, ao_overlap: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, \"[m, n]\"] | None = None, basis_set: qdk_chemistry._core.data.BasisSet, indices: tuple[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]] | None = None)\n\nInvoked with: array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],\n [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]), None, None" - ] - } - ], "source": [ - "from random_wavefunction import generate_random_wavefunction\n", + "## Estimating the energy on a quantum computer\n", "\n", - "num_qubits = 26\n", - "seed = 42\n", - "wfn = generate_random_wavefunction(n_electrons=num_qubits//2, n_orbitals=num_qubits//2, n_dets=num_qubits, seed=seed)" + "For the final stage of this state preparation application benchmark workflow, we estimate the energy of the optimized multi-configuration wavefunction prepared on a quantum computer.\n", + "The first step in this process is mapping the classical Hamiltonian for the active space to a qubit Hamiltonian that can be measured on a quantum computer.\n", + "For this example, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation) to perform this mapping." ] }, { "cell_type": "code", "execution_count": null, - "id": "76edbb9c", + "id": "2f856f41", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[2026-04-02 17:15:44.716994] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", - "[2026-04-02 17:15:44.727282] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", - "[2026-04-02 17:15:44.735311] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", - "[2026-04-02 17:15:44.744733] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", - "[2026-04-02 17:15:44.751399] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n", - "[2026-04-02 17:15:44.784098] [info] [sparse_isometry_binary_encoding] Binary encoding produced 20 operations (20 encoded) using 0 ancillae for 12-qubit system with 8 determinants\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2a5bb763df4b4c00ab1d18c2f29b3365", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Logical EstimateCounts
0numQubits14
1tCount7
2rotationCount7
3rotationDepth7
4cczCount7
5ccixCount0
6measurementCount6
\n", - "
" - ], - "text/plain": [ - " Logical Estimate Counts\n", - "0 numQubits 14\n", - "1 tCount 7\n", - "2 rotationCount 7\n", - "3 rotationDepth 7\n", - "4 cczCount 7\n", - "5 ccixCount 0\n", - "6 measurementCount 6" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "from sparse_isometry_binary_encoding import SparseIsometryBinaryEncodingStatePreparation\n", - "\n", - "sp_select = SparseIsometryBinaryEncodingStatePreparation()\n", - "sp_select.settings().update(\"use_measurement_and\", True)\n", - "select_circuit = sp_select.run(wfn_sparse)\n", - "display(Circuit(qsharp.circuit(select_circuit._qsharp_factory.program, *select_circuit._qsharp_factory.parameter.values(), generation_method=qsharp.CircuitGenerationMethod.Static)))\n", - "\n", - "df = pd.DataFrame(\n", - " qsharp.estimate(select_circuit._qsharp_factory.program, None, *select_circuit._qsharp_factory.parameter.values()).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", - "display(df)" + "# Prepare qubit Hamiltonian\n", + "qubit_mapper = create(\"qubit_mapper\", algorithm_name=\"qiskit\", encoding=\"jordan-wigner\")\n", + "qubit_hamiltonian = qubit_mapper.run(hamiltonian)" ] }, { @@ -818,18 +429,18 @@ { "cell_type": "code", "execution_count": null, - "id": "62d03ae3", + "id": "3e9b6616", "metadata": {}, "outputs": [], "source": [ "# Estimate energy using the optimized circuit and the qubit Hamiltonian\n", "estimator = create(\"energy_estimator\", algorithm_name=\"qdk\")\n", - "circuit_executor = create(\"circuit_executor\", algorithm_name=\"qdk_full_state_simulator\")\n", + "circuit_executor = create(\"circuit_executor\", algorithm_name=\"qiskit_aer_simulator\")\n", "energy_results, simulation_data = estimator.run(\n", - " circuit=sparse_isometry_circuit,\n", + " circuit=sparse_isometry_binary_encoding_circuit,\n", " qubit_hamiltonian=qubit_hamiltonian,\n", " circuit_executor=circuit_executor,\n", - " total_shots=1500000,\n", + " total_shots=2000000,\n", ")\n", "\n", "# Print statistic for measured energy\n", @@ -842,120 +453,11 @@ "# Print comparison with reference energy\n", "print(f\"Difference from reference energy: {energy_mean - E_sparse} Hartree\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9594e049", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "numQubits: 12\n", - "numAncilla: 0\n", - "rowMap: [0, 2, 1]\n", - "stateVector length: 8\n", - "expansionOps count: 28\n", - "binaryEncodingOps count: 20\n", - "\n", - "--- Expansion Ops ---\n", - " [0] CX qubits=[4, 11]\n", - " [1] CX qubits=[4, 9]\n", - " [2] CX qubits=[4, 8]\n", - " [3] CX qubits=[4, 7]\n", - " [4] CX qubits=[4, 3]\n", - " [5] CX qubits=[4, 1]\n", - " [6] CX qubits=[3, 11]\n", - " [7] CX qubits=[3, 9]\n", - " [8] CX qubits=[3, 5]\n", - " [9] CX qubits=[3, 4]\n", - " ... (28 total)\n", - "\n", - "--- Binary Encoding Ops ---\n", - " [0] SELECT qubits=[0, 2, 1, 3] lookupData=8 rows\n", - " [1] CX qubits=[3, 1] lookupData=0 rows\n", - " [2] SELECT qubits=[0, 2, 1, 3] lookupData=8 rows\n", - " [3] CX qubits=[3, 2] lookupData=0 rows\n", - " [4] SELECT qubits=[0, 2, 1, 3] lookupData=8 rows\n", - " [5] CX qubits=[3, 1] lookupData=0 rows\n", - " [6] CX qubits=[3, 2] lookupData=0 rows\n", - " [7] CX qubits=[3, 0] lookupData=0 rows\n", - " [8] SWAP qubits=[2, 1] lookupData=0 rows\n", - " [9] SWAP qubits=[0, 4] lookupData=0 rows\n", - " ... (20 total)\n", - "\n", - "--- GF2+X result operations (from gf2x_result) ---\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[2026-04-02 15:47:10.039960] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Original matrix rank: 5\n", - "[2026-04-02 15:47:10.062632] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 6 identical to row 0, adding CX(0, 6)\n", - "[2026-04-02 15:47:10.074259] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Found duplicate row 10 identical to row 4, adding CX(4, 10)\n", - "[2026-04-02 15:47:10.083060] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Eliminating 2 duplicate rows: [6, 10]\n", - "[2026-04-02 15:47:10.099927] [info] [qdk_chemistry.algorithms.state_preparation.sparse_isometry] Final reduced matrix rank: 5\n", - "Total GF2+X operations: 28\n", - "Operation types: {'cx': 28}\n", - "row_map: [0, 2, 1, 3, 4]\n" - ] - } - ], - "source": [ - "# Diagnostic: inspect what the circuit parameters contain\n", - "params = circuit._qsharp_factory.parameter\n", - "print(f\"numQubits: {params['numQubits']}\")\n", - "print(f\"numAncilla: {params['numAncilla']}\")\n", - "print(f\"rowMap: {params['rowMap']}\")\n", - "print(f\"stateVector length: {len(params['stateVector'])}\")\n", - "print(f\"expansionOps count: {len(params['expansionOps'])}\")\n", - "print(f\"binaryEncodingOps count: {len(params['binaryEncodingOps'])}\")\n", - "\n", - "# Show expansion ops\n", - "print(\"\\n--- Expansion Ops ---\")\n", - "for i, op in enumerate(params['expansionOps'][:10]):\n", - " print(f\" [{i}] {op['name']} qubits={op['qubits']}\")\n", - "if len(params['expansionOps']) > 10:\n", - " print(f\" ... ({len(params['expansionOps'])} total)\")\n", - "\n", - "# Show binary encoding ops\n", - "print(\"\\n--- Binary Encoding Ops ---\")\n", - "for i, op in enumerate(params['binaryEncodingOps'][:10]):\n", - " print(f\" [{i}] {op['name']} qubits={op['qubits']} lookupData={len(op.get('lookupData', [])) if op.get('lookupData') else 0} rows\")\n", - "if len(params['binaryEncodingOps']) > 10:\n", - " print(f\" ... ({len(params['binaryEncodingOps'])} total)\")\n", - "\n", - "# Check GF2+X operations directly\n", - "print(\"\\n--- GF2+X result operations (from gf2x_result) ---\")\n", - "gf2x_result, _ = sp._perform_gf2x(\n", - " [beta_str[::-1] + alpha_str[::-1]\n", - " for det in wfn_sparse.get_active_determinants()\n", - " for alpha_str, beta_str in [det.to_binary_strings(num_orbitals)]],\n", - " wfn_sparse.get_coefficients()\n", - ")\n", - "print(f\"Total GF2+X operations: {len(gf2x_result.operations)}\")\n", - "op_types = {}\n", - "for op in gf2x_result.operations:\n", - " op_types[op[0]] = op_types.get(op[0], 0) + 1\n", - "print(f\"Operation types: {op_types}\")\n", - "print(f\"row_map: {gf2x_result.row_map}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d55a388b", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "qdk_chemistry_venv (3.13.12)", + "display_name": "qdk_chemistry_venv (3.12.3)", "language": "python", "name": "python3" }, @@ -969,7 +471,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.12" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/examples/state_prep_sparse_isometry.ipynb b/examples/state_prep_sparse_isometry.ipynb new file mode 100644 index 000000000..c10256dc0 --- /dev/null +++ b/examples/state_prep_sparse_isometry.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "5f173257", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import pandas as pd\n", + "\n", + "from qdk_chemistry.algorithms import create\n", + "from qdk_chemistry.data import Structure\n", + "from qdk_chemistry.utils import Logger, compute_valence_space_parameters\n", + "from qdk_chemistry.utils.wavefunction import (\n", + " calculate_sparse_wavefunction,\n", + " get_active_determinants_info,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2344255b", + "metadata": {}, + "outputs": [], + "source": [ + "structure_path = Path.cwd() / \"data\" / \"ozone.structure.xyz\"\n", + "structure = Structure.from_xyz_file(structure_path)\n", + "Logger.info(structure.get_summary())\n", + "\n", + "scf_solver = create(\"scf_solver\")\n", + "e_scf, scf_wavefunction = scf_solver.run(structure, 0, 1, \"cc-pvdz\")\n", + "Logger.info(f\"SCF energy = {e_scf:.8f} Hartree\")\n", + "\n", + "electrons, orbitals = compute_valence_space_parameters(scf_wavefunction, 0)\n", + "selector = create(\"active_space_selector\", \"qdk_valence\")\n", + "settings = selector.settings()\n", + "settings.set(\"num_active_electrons\", electrons)\n", + "settings.set(\"num_active_orbitals\", orbitals)\n", + "\n", + "active_orbital_wavefunction = selector.run(scf_wavefunction)\n", + "active_orbitals = active_orbital_wavefunction.get_orbitals()\n", + "Logger.info(active_orbitals.get_summary())\n", + "\n", + "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", + "active_hamiltonian = hamiltonian_constructor.run(active_orbitals)\n", + "Logger.info(active_hamiltonian.get_summary())\n", + "\n", + "casci_calculator = create(\n", + " \"multi_configuration_calculator\", \"macis_asci\"\n", + ")\n", + "casci_calculator.settings().set(\"calculate_one_rdm\", True)\n", + "casci_calculator.settings().set(\"calculate_two_rdm\", True)\n", + "casci_calculator.settings().set(\"core_selection_strategy\", \"fixed\")\n", + "e_cas, wfn_cas = casci_calculator.run(\n", + " active_hamiltonian, *active_orbital_wavefunction.get_active_num_electrons()\n", + ")\n", + "Logger.info(f\"CASCI energy = {e_cas:.8f} Hartree\")\n", + "\n", + "autocas_selector = create(\"active_space_selector\", \"qdk_autocas\")\n", + "refined_wfn = autocas_selector.run(wfn_cas)\n", + "indices, _ = refined_wfn.get_orbitals().get_active_space_indices()\n", + "Logger.info(f\"autoCAS selected active space with indices: {indices}\")\n", + "\n", + "refined_orbitals = refined_wfn.get_orbitals()\n", + "active_hamiltonian = hamiltonian_constructor.run(refined_orbitals)\n", + "e_cas, wfn_cas = casci_calculator.run(\n", + " active_hamiltonian, *refined_wfn.get_active_num_electrons()\n", + ")\n", + "Logger.info(active_hamiltonian.get_summary())\n", + "Logger.info(f\"autoCAS energy = {e_cas:.8f} Hartree\")\n", + "\n", + "sparse_ci_energy, sparse_ci_wavefunction = calculate_sparse_wavefunction(\n", + " reference_wavefunction=wfn_cas,\n", + " hamiltonian=active_hamiltonian,\n", + " reference_energy=e_cas,\n", + " energy_tolerance=1.0e-3,\n", + " max_determinants=100,\n", + ")\n", + "\n", + "Logger.info(f\"Sparse CI energy = {sparse_ci_energy:.8f} Hartree\")\n", + "Logger.info(get_active_determinants_info(sparse_ci_wavefunction))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49a6694d", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk.widgets import Circuit\n", + "\n", + "sparse_isometry_gf2x = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "\n", + "gf2x_circuit = sparse_isometry_gf2x.run(sparse_ci_wavefunction)\n", + "display(Circuit(gf2x_circuit.get_qsharp_circuit()))\n", + "\n", + "# Print logical qubit counts estimated from the circuit\n", + "df = pd.DataFrame(\n", + " gf2x_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "display(df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "541fa46b", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk.widgets import Circuit\n", + "\n", + "sparse_isometry_bin_enc = create(\"state_prep\", \"sparse_isometry_binary_encoding\", measurement_based_uncompute=False)\n", + "\n", + "bin_enc_circuit = sparse_isometry_bin_enc.run(sparse_ci_wavefunction)\n", + "#display(Circuit(bin_enc_circuit.get_qsharp_circuit()))\n", + "\n", + "# Print logical qubit counts estimated from the circuit\n", + "df = pd.DataFrame(\n", + " bin_enc_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "display(df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c536b44", + "metadata": {}, + "outputs": [], + "source": [ + "from utils.state_prep_utils import filter_and_group_pauli_ops_from_wavefunction\n", + "from qdk_chemistry.data import QubitHamiltonian\n", + "\n", + "qubit_mapper = create(\"qubit_mapper\", \"qdk\")\n", + "qubit_hamiltonian = qubit_mapper.run(active_hamiltonian)\n", + "\n", + "pauli_strings =[]\n", + "coeffs = []\n", + "filtered_ops, classical_coeffs = filter_and_group_pauli_ops_from_wavefunction(qubit_hamiltonian, sparse_ci_wavefunction)\n", + "for op, _ in zip(filtered_ops, classical_coeffs):\n", + " pauli_strings.extend(op.pauli_strings)\n", + " coeffs.extend(op.coefficients)\n", + "filtered_hamiltonian = QubitHamiltonian(pauli_strings, coeffs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f7c4bfe", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "energy_estimator = create(\"energy_estimator\", \"qdk\")\n", + "simulator = create(\"circuit_executor\", \"qiskit_aer_simulator\")\n", + "\n", + "results, _ = energy_estimator.run(bin_enc_circuit, filtered_hamiltonian, simulator, 240000)\n", + "\n", + "# Print statistic for measured energy\n", + "energy_mean = results.energy_expectation_value + active_hamiltonian.get_core_energy() + sum(classical_coeffs)\n", + "energy_stddev = np.sqrt(results.energy_variance)\n", + "print(\n", + " f\"Estimated energy from quantum circuit: {energy_mean:.3f} ± {energy_stddev:.3f} Hartree\"\n", + ")\n", + "\n", + "# Print comparison with reference energy\n", + "print(f\"Difference from reference energy: {energy_mean - sparse_ci_energy} Hartree\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a434d02c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qdk_chemistry_venv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/utils/state_prep_utils.py b/examples/utils/state_prep_utils.py new file mode 100644 index 000000000..c71654c52 --- /dev/null +++ b/examples/utils/state_prep_utils.py @@ -0,0 +1,202 @@ +"""Utility functions for state preparation.""" +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import numpy as np +from qdk_chemistry.data import QubitHamiltonian, Wavefunction +from qdk_chemistry.utils import Logger +from qdk_chemistry.utils.pauli_matrix import pauli_string_to_masks + + +def pauli_expectation(pauli_str: str, psi: np.ndarray) -> float: + """Compute the expectation value ```` for a single Pauli string. + + Args: + pauli_str: Pauli label of length *n* (characters in {I, X, Y, Z}), using Little-Endian convention. + psi: Complex state vector of length ``2**n``. + + Returns: + Expectation value. + + """ + psi = np.asarray(psi, dtype=np.complex128).ravel() + expected_dim = 1 << len(pauli_str) + if psi.size != expected_dim: + raise ValueError( + f"State vector length {psi.size} does not match the expected " + f"dimension 2**{len(pauli_str)} = {expected_dim} for Pauli string '{pauli_str}'." + ) + x_mask, z_mask, phase = pauli_string_to_masks(pauli_str) + dim = psi.shape[0] + + # Build the row-index array and the corresponding column-index array + rows = np.arange(dim, dtype=np.int64) + cols = rows ^ x_mask # P maps |r> -> phase * |r ^ x_mask> + + # Parity of (col & z_mask) gives the sign: (-1)^popcount(col & z_mask) + parity_bits = cols & z_mask + signs = np.where(np.bitwise_count(parity_bits) & 1, -1.0, 1.0) + + val = np.sum(np.conj(psi[rows]) * phase * signs * psi[cols]) + return float(val.real) + + +def _filter_and_group_pauli_ops_from_statevector( + hamiltonian: QubitHamiltonian, + statevector: np.ndarray, + abelian_grouping: bool = True, + trimming: bool = True, + trimming_tolerance: float = 1e-8, +) -> tuple[list[QubitHamiltonian], list[float]]: + """Filter and group the Pauli operators respect to a given quantum state. + + This function evaluates each Pauli term in the Hamiltonian with respect to the + provided statevector: + + * Terms with zero expectation value are discarded. + * Terms with expectation ±1 are treated as classical and their contribution is + added to the energy at the end. + * Remaining terms with fractional expectation values are retained and grouped by + shared expectation value to reduce measurement redundancy + (e.g., due to symmetry). + * The rest of Hamiltonian is grouped into qubit wise commuting terms. + + Args: + hamiltonian (QubitHamiltonian): QubitHamiltonian to be filtered and grouped. + statevector (numpy.ndarray): Statevector used to compute expectation values. + abelian_grouping (bool): Whether to group into qubit-wise commuting subsets. + trimming (bool): If True, discard or reduce terms with ±1 or 0 expectation value. + trimming_tolerance (float): Numerical tolerance for determining zero or ±1 expectation (Default: 1e-8). + + Returns: + A tuple of ``(list[QubitHamiltonian], list[float])`` + * A list of grouped QubitHamiltonian. + * A list of classical coefficients for terms that were reduced to classical contributions. + + """ + Logger.trace_entering() + psi = np.asarray(statevector, dtype=complex) + norm = np.linalg.norm(psi) + if norm < np.finfo(np.float64).eps: + raise ValueError("Statevector has zero norm.") + psi /= norm + + retained_paulis: list[str] = [] + retained_coeffs: list[complex] = [] + expectations: list[float] = [] + classical: list[float] = [] + + for pauli_str, coeff in zip( + hamiltonian.pauli_strings, hamiltonian.coefficients, strict=True + ): + expval = pauli_expectation(pauli_str, psi) + + if not trimming: + retained_paulis.append(pauli_str) + retained_coeffs.append(coeff) + expectations.append(expval) + continue + + if np.isclose(expval, 0.0, atol=trimming_tolerance): + continue + if np.isclose(expval, 1.0, atol=trimming_tolerance): + classical.append(float(coeff.real)) + elif np.isclose(expval, -1.0, atol=trimming_tolerance): + classical.append(float(-coeff.real)) + else: + retained_paulis.append(pauli_str) + retained_coeffs.append(coeff) + expectations.append(expval) + + if not retained_paulis: + return [], classical + + grouped: dict[int, list[tuple[str, complex, float]]] = {} + key_counter = 0 + # Assign approximate groups based on tolerance + for pauli, coeff, expval in zip( + retained_paulis, retained_coeffs, expectations, strict=True + ): + matched_key = None + for k, terms in grouped.items(): + if np.isclose(expval, terms[0][2], atol=trimming_tolerance): + matched_key = k + break + if matched_key is None: + grouped[key_counter] = [(pauli, coeff, expval)] + key_counter += 1 + else: + grouped[matched_key].append((pauli, coeff, expval)) + + reduced_pauli: list[str] = [] + reduced_coeffs: list[complex] = [] + + for _, terms in grouped.items(): + coeff_sum = sum(c for _, c, _ in terms) + # Choose Pauli with maximum # of I (most diagonal) + best_pauli = sorted( + [p for p, _, _ in terms], key=lambda p: (-str(p).count("I"), str(p)) + )[0] + reduced_pauli.append(best_pauli) + reduced_coeffs.append(coeff_sum) + + reduced_hamiltonian = QubitHamiltonian( + reduced_pauli, + np.array(reduced_coeffs), + encoding=hamiltonian.encoding, + fermion_mode_order=hamiltonian.fermion_mode_order, + ) + + grouped_hamiltonians = ( + reduced_hamiltonian.group_commuting(qubit_wise=abelian_grouping) + if abelian_grouping + else [reduced_hamiltonian] + ) + + return grouped_hamiltonians, classical + + +def filter_and_group_pauli_ops_from_wavefunction( + hamiltonian: QubitHamiltonian, + wavefunction: Wavefunction, + abelian_grouping: bool = True, + trimming: bool = True, + trimming_tolerance: float = 1e-8, +) -> tuple[list[QubitHamiltonian], list[float]]: + """Filter and group the Pauli operators respect to a given quantum state. + + This function evaluates each Pauli term in the Hamiltonian with respect to the + provided wavefunction: + + * Terms with zero expectation value are discarded. + * Terms with expectation ±1 are treated as classical and their contribution is + added to the energy at the end. + * Remaining terms with fractional expectation values are retained and grouped by + shared expectation value to reduce measurement redundancy + (e.g., due to symmetry). + * The rest of Hamiltonian is grouped into qubit wise commuting terms. + + Args: + hamiltonian (QubitHamiltonian): QubitHamiltonian to be filtered and grouped. + wavefunction (Wavefunction): Wavefunction used to compute expectation values. + abelian_grouping (bool): Whether to group into qubit-wise commuting subsets. + trimming (bool): If True, discard or reduce terms with ±1 or 0 expectation value. + trimming_tolerance (float): Numerical tolerance for determining zero or ±1 expectation (Default: 1e-8). + + Returns: + A tuple of ``(list[QubitHamiltonian], list[float])`` + * A list of grouped QubitHamiltonian. + * A list of classical coefficients for terms that were reduced to classical contributions. + + """ + from qdk_chemistry.plugins.qiskit.conversion import ( + create_statevector_from_wavefunction, # noqa: PLC0415 + ) + + Logger.trace_entering() + psi = create_statevector_from_wavefunction(wavefunction, normalize=True) + return _filter_and_group_pauli_ops_from_statevector( + hamiltonian, psi, abelian_grouping, trimming, trimming_tolerance + ) diff --git a/python/tests/test_state_preparation_binary_encoding.py b/python/tests/test_state_preparation_binary_encoding.py index 69af221fd..30ccb61b2 100644 --- a/python/tests/test_state_preparation_binary_encoding.py +++ b/python/tests/test_state_preparation_binary_encoding.py @@ -1,11 +1,4 @@ -"""Tests for the sparse isometry with binary encoding state preparation. - -All tests live in a single class ``TestSparseIsometryBinaryEncoding``. -Two main tests exercise the full ``run()`` → ``Circuit`` → ``estimate()`` -pipeline on the ozone SCI wavefunction and random wavefunctions. The -remaining tests cover settings, single-determinant fast-path, scaling, -``_encode_gf2x_ops_for_qs`` unit tests, and algorithm metadata. -""" +"""Tests for the sparse isometry with binary encoding state preparation.""" # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. From a93946f6767a261d0ad397690654c7d4d901bb6f Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Mon, 6 Apr 2026 22:57:20 +0000 Subject: [PATCH 04/21] remove redundant demo notebook --- examples/data/ozone.structure.xyz | 7 - examples/state_prep_sparse_isometry.ipynb | 204 ---------------------- examples/utils/state_prep_utils.py | 202 --------------------- 3 files changed, 413 deletions(-) delete mode 100644 examples/data/ozone.structure.xyz delete mode 100644 examples/state_prep_sparse_isometry.ipynb delete mode 100644 examples/utils/state_prep_utils.py diff --git a/examples/data/ozone.structure.xyz b/examples/data/ozone.structure.xyz deleted file mode 100644 index 07078d65e..000000000 --- a/examples/data/ozone.structure.xyz +++ /dev/null @@ -1,7 +0,0 @@ -3 -Coordinates from ORCA-job singlet_O3 E -225.342972028963 - O 1.23739606886123 -0.26557802176941 0.00000000000000 - O 0.02818212873195 0.02818144808773 0.00000000000000 - O -0.26557819759318 1.23739657368168 0.00000000000000 - - diff --git a/examples/state_prep_sparse_isometry.ipynb b/examples/state_prep_sparse_isometry.ipynb deleted file mode 100644 index c10256dc0..000000000 --- a/examples/state_prep_sparse_isometry.ipynb +++ /dev/null @@ -1,204 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "5f173257", - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import pandas as pd\n", - "\n", - "from qdk_chemistry.algorithms import create\n", - "from qdk_chemistry.data import Structure\n", - "from qdk_chemistry.utils import Logger, compute_valence_space_parameters\n", - "from qdk_chemistry.utils.wavefunction import (\n", - " calculate_sparse_wavefunction,\n", - " get_active_determinants_info,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2344255b", - "metadata": {}, - "outputs": [], - "source": [ - "structure_path = Path.cwd() / \"data\" / \"ozone.structure.xyz\"\n", - "structure = Structure.from_xyz_file(structure_path)\n", - "Logger.info(structure.get_summary())\n", - "\n", - "scf_solver = create(\"scf_solver\")\n", - "e_scf, scf_wavefunction = scf_solver.run(structure, 0, 1, \"cc-pvdz\")\n", - "Logger.info(f\"SCF energy = {e_scf:.8f} Hartree\")\n", - "\n", - "electrons, orbitals = compute_valence_space_parameters(scf_wavefunction, 0)\n", - "selector = create(\"active_space_selector\", \"qdk_valence\")\n", - "settings = selector.settings()\n", - "settings.set(\"num_active_electrons\", electrons)\n", - "settings.set(\"num_active_orbitals\", orbitals)\n", - "\n", - "active_orbital_wavefunction = selector.run(scf_wavefunction)\n", - "active_orbitals = active_orbital_wavefunction.get_orbitals()\n", - "Logger.info(active_orbitals.get_summary())\n", - "\n", - "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", - "active_hamiltonian = hamiltonian_constructor.run(active_orbitals)\n", - "Logger.info(active_hamiltonian.get_summary())\n", - "\n", - "casci_calculator = create(\n", - " \"multi_configuration_calculator\", \"macis_asci\"\n", - ")\n", - "casci_calculator.settings().set(\"calculate_one_rdm\", True)\n", - "casci_calculator.settings().set(\"calculate_two_rdm\", True)\n", - "casci_calculator.settings().set(\"core_selection_strategy\", \"fixed\")\n", - "e_cas, wfn_cas = casci_calculator.run(\n", - " active_hamiltonian, *active_orbital_wavefunction.get_active_num_electrons()\n", - ")\n", - "Logger.info(f\"CASCI energy = {e_cas:.8f} Hartree\")\n", - "\n", - "autocas_selector = create(\"active_space_selector\", \"qdk_autocas\")\n", - "refined_wfn = autocas_selector.run(wfn_cas)\n", - "indices, _ = refined_wfn.get_orbitals().get_active_space_indices()\n", - "Logger.info(f\"autoCAS selected active space with indices: {indices}\")\n", - "\n", - "refined_orbitals = refined_wfn.get_orbitals()\n", - "active_hamiltonian = hamiltonian_constructor.run(refined_orbitals)\n", - "e_cas, wfn_cas = casci_calculator.run(\n", - " active_hamiltonian, *refined_wfn.get_active_num_electrons()\n", - ")\n", - "Logger.info(active_hamiltonian.get_summary())\n", - "Logger.info(f\"autoCAS energy = {e_cas:.8f} Hartree\")\n", - "\n", - "sparse_ci_energy, sparse_ci_wavefunction = calculate_sparse_wavefunction(\n", - " reference_wavefunction=wfn_cas,\n", - " hamiltonian=active_hamiltonian,\n", - " reference_energy=e_cas,\n", - " energy_tolerance=1.0e-3,\n", - " max_determinants=100,\n", - ")\n", - "\n", - "Logger.info(f\"Sparse CI energy = {sparse_ci_energy:.8f} Hartree\")\n", - "Logger.info(get_active_determinants_info(sparse_ci_wavefunction))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "49a6694d", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.widgets import Circuit\n", - "\n", - "sparse_isometry_gf2x = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", - "\n", - "gf2x_circuit = sparse_isometry_gf2x.run(sparse_ci_wavefunction)\n", - "display(Circuit(gf2x_circuit.get_qsharp_circuit()))\n", - "\n", - "# Print logical qubit counts estimated from the circuit\n", - "df = pd.DataFrame(\n", - " gf2x_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", - "display(df)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "541fa46b", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.widgets import Circuit\n", - "\n", - "sparse_isometry_bin_enc = create(\"state_prep\", \"sparse_isometry_binary_encoding\", measurement_based_uncompute=False)\n", - "\n", - "bin_enc_circuit = sparse_isometry_bin_enc.run(sparse_ci_wavefunction)\n", - "#display(Circuit(bin_enc_circuit.get_qsharp_circuit()))\n", - "\n", - "# Print logical qubit counts estimated from the circuit\n", - "df = pd.DataFrame(\n", - " bin_enc_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", - "display(df)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5c536b44", - "metadata": {}, - "outputs": [], - "source": [ - "from utils.state_prep_utils import filter_and_group_pauli_ops_from_wavefunction\n", - "from qdk_chemistry.data import QubitHamiltonian\n", - "\n", - "qubit_mapper = create(\"qubit_mapper\", \"qdk\")\n", - "qubit_hamiltonian = qubit_mapper.run(active_hamiltonian)\n", - "\n", - "pauli_strings =[]\n", - "coeffs = []\n", - "filtered_ops, classical_coeffs = filter_and_group_pauli_ops_from_wavefunction(qubit_hamiltonian, sparse_ci_wavefunction)\n", - "for op, _ in zip(filtered_ops, classical_coeffs):\n", - " pauli_strings.extend(op.pauli_strings)\n", - " coeffs.extend(op.coefficients)\n", - "filtered_hamiltonian = QubitHamiltonian(pauli_strings, coeffs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3f7c4bfe", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "energy_estimator = create(\"energy_estimator\", \"qdk\")\n", - "simulator = create(\"circuit_executor\", \"qiskit_aer_simulator\")\n", - "\n", - "results, _ = energy_estimator.run(bin_enc_circuit, filtered_hamiltonian, simulator, 240000)\n", - "\n", - "# Print statistic for measured energy\n", - "energy_mean = results.energy_expectation_value + active_hamiltonian.get_core_energy() + sum(classical_coeffs)\n", - "energy_stddev = np.sqrt(results.energy_variance)\n", - "print(\n", - " f\"Estimated energy from quantum circuit: {energy_mean:.3f} ± {energy_stddev:.3f} Hartree\"\n", - ")\n", - "\n", - "# Print comparison with reference energy\n", - "print(f\"Difference from reference energy: {energy_mean - sparse_ci_energy} Hartree\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a434d02c", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "qdk_chemistry_venv (3.12.3)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/utils/state_prep_utils.py b/examples/utils/state_prep_utils.py deleted file mode 100644 index c71654c52..000000000 --- a/examples/utils/state_prep_utils.py +++ /dev/null @@ -1,202 +0,0 @@ -"""Utility functions for state preparation.""" -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -import numpy as np -from qdk_chemistry.data import QubitHamiltonian, Wavefunction -from qdk_chemistry.utils import Logger -from qdk_chemistry.utils.pauli_matrix import pauli_string_to_masks - - -def pauli_expectation(pauli_str: str, psi: np.ndarray) -> float: - """Compute the expectation value ```` for a single Pauli string. - - Args: - pauli_str: Pauli label of length *n* (characters in {I, X, Y, Z}), using Little-Endian convention. - psi: Complex state vector of length ``2**n``. - - Returns: - Expectation value. - - """ - psi = np.asarray(psi, dtype=np.complex128).ravel() - expected_dim = 1 << len(pauli_str) - if psi.size != expected_dim: - raise ValueError( - f"State vector length {psi.size} does not match the expected " - f"dimension 2**{len(pauli_str)} = {expected_dim} for Pauli string '{pauli_str}'." - ) - x_mask, z_mask, phase = pauli_string_to_masks(pauli_str) - dim = psi.shape[0] - - # Build the row-index array and the corresponding column-index array - rows = np.arange(dim, dtype=np.int64) - cols = rows ^ x_mask # P maps |r> -> phase * |r ^ x_mask> - - # Parity of (col & z_mask) gives the sign: (-1)^popcount(col & z_mask) - parity_bits = cols & z_mask - signs = np.where(np.bitwise_count(parity_bits) & 1, -1.0, 1.0) - - val = np.sum(np.conj(psi[rows]) * phase * signs * psi[cols]) - return float(val.real) - - -def _filter_and_group_pauli_ops_from_statevector( - hamiltonian: QubitHamiltonian, - statevector: np.ndarray, - abelian_grouping: bool = True, - trimming: bool = True, - trimming_tolerance: float = 1e-8, -) -> tuple[list[QubitHamiltonian], list[float]]: - """Filter and group the Pauli operators respect to a given quantum state. - - This function evaluates each Pauli term in the Hamiltonian with respect to the - provided statevector: - - * Terms with zero expectation value are discarded. - * Terms with expectation ±1 are treated as classical and their contribution is - added to the energy at the end. - * Remaining terms with fractional expectation values are retained and grouped by - shared expectation value to reduce measurement redundancy - (e.g., due to symmetry). - * The rest of Hamiltonian is grouped into qubit wise commuting terms. - - Args: - hamiltonian (QubitHamiltonian): QubitHamiltonian to be filtered and grouped. - statevector (numpy.ndarray): Statevector used to compute expectation values. - abelian_grouping (bool): Whether to group into qubit-wise commuting subsets. - trimming (bool): If True, discard or reduce terms with ±1 or 0 expectation value. - trimming_tolerance (float): Numerical tolerance for determining zero or ±1 expectation (Default: 1e-8). - - Returns: - A tuple of ``(list[QubitHamiltonian], list[float])`` - * A list of grouped QubitHamiltonian. - * A list of classical coefficients for terms that were reduced to classical contributions. - - """ - Logger.trace_entering() - psi = np.asarray(statevector, dtype=complex) - norm = np.linalg.norm(psi) - if norm < np.finfo(np.float64).eps: - raise ValueError("Statevector has zero norm.") - psi /= norm - - retained_paulis: list[str] = [] - retained_coeffs: list[complex] = [] - expectations: list[float] = [] - classical: list[float] = [] - - for pauli_str, coeff in zip( - hamiltonian.pauli_strings, hamiltonian.coefficients, strict=True - ): - expval = pauli_expectation(pauli_str, psi) - - if not trimming: - retained_paulis.append(pauli_str) - retained_coeffs.append(coeff) - expectations.append(expval) - continue - - if np.isclose(expval, 0.0, atol=trimming_tolerance): - continue - if np.isclose(expval, 1.0, atol=trimming_tolerance): - classical.append(float(coeff.real)) - elif np.isclose(expval, -1.0, atol=trimming_tolerance): - classical.append(float(-coeff.real)) - else: - retained_paulis.append(pauli_str) - retained_coeffs.append(coeff) - expectations.append(expval) - - if not retained_paulis: - return [], classical - - grouped: dict[int, list[tuple[str, complex, float]]] = {} - key_counter = 0 - # Assign approximate groups based on tolerance - for pauli, coeff, expval in zip( - retained_paulis, retained_coeffs, expectations, strict=True - ): - matched_key = None - for k, terms in grouped.items(): - if np.isclose(expval, terms[0][2], atol=trimming_tolerance): - matched_key = k - break - if matched_key is None: - grouped[key_counter] = [(pauli, coeff, expval)] - key_counter += 1 - else: - grouped[matched_key].append((pauli, coeff, expval)) - - reduced_pauli: list[str] = [] - reduced_coeffs: list[complex] = [] - - for _, terms in grouped.items(): - coeff_sum = sum(c for _, c, _ in terms) - # Choose Pauli with maximum # of I (most diagonal) - best_pauli = sorted( - [p for p, _, _ in terms], key=lambda p: (-str(p).count("I"), str(p)) - )[0] - reduced_pauli.append(best_pauli) - reduced_coeffs.append(coeff_sum) - - reduced_hamiltonian = QubitHamiltonian( - reduced_pauli, - np.array(reduced_coeffs), - encoding=hamiltonian.encoding, - fermion_mode_order=hamiltonian.fermion_mode_order, - ) - - grouped_hamiltonians = ( - reduced_hamiltonian.group_commuting(qubit_wise=abelian_grouping) - if abelian_grouping - else [reduced_hamiltonian] - ) - - return grouped_hamiltonians, classical - - -def filter_and_group_pauli_ops_from_wavefunction( - hamiltonian: QubitHamiltonian, - wavefunction: Wavefunction, - abelian_grouping: bool = True, - trimming: bool = True, - trimming_tolerance: float = 1e-8, -) -> tuple[list[QubitHamiltonian], list[float]]: - """Filter and group the Pauli operators respect to a given quantum state. - - This function evaluates each Pauli term in the Hamiltonian with respect to the - provided wavefunction: - - * Terms with zero expectation value are discarded. - * Terms with expectation ±1 are treated as classical and their contribution is - added to the energy at the end. - * Remaining terms with fractional expectation values are retained and grouped by - shared expectation value to reduce measurement redundancy - (e.g., due to symmetry). - * The rest of Hamiltonian is grouped into qubit wise commuting terms. - - Args: - hamiltonian (QubitHamiltonian): QubitHamiltonian to be filtered and grouped. - wavefunction (Wavefunction): Wavefunction used to compute expectation values. - abelian_grouping (bool): Whether to group into qubit-wise commuting subsets. - trimming (bool): If True, discard or reduce terms with ±1 or 0 expectation value. - trimming_tolerance (float): Numerical tolerance for determining zero or ±1 expectation (Default: 1e-8). - - Returns: - A tuple of ``(list[QubitHamiltonian], list[float])`` - * A list of grouped QubitHamiltonian. - * A list of classical coefficients for terms that were reduced to classical contributions. - - """ - from qdk_chemistry.plugins.qiskit.conversion import ( - create_statevector_from_wavefunction, # noqa: PLC0415 - ) - - Logger.trace_entering() - psi = create_statevector_from_wavefunction(wavefunction, normalize=True) - return _filter_and_group_pauli_ops_from_statevector( - hamiltonian, psi, abelian_grouping, trimming, trimming_tolerance - ) From 8a87f8bc0a7e856b70986f220dafc0b058100e77 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Mon, 6 Apr 2026 23:16:43 +0000 Subject: [PATCH 05/21] remove redundant qsharp init --- python/src/qdk_chemistry/__init__.py | 17 ----------------- .../src/qdk_chemistry/utils/qsharp/__init__.py | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/python/src/qdk_chemistry/__init__.py b/python/src/qdk_chemistry/__init__.py index f5b7d9c7e..59ae1c38c 100644 --- a/python/src/qdk_chemistry/__init__.py +++ b/python/src/qdk_chemistry/__init__.py @@ -26,10 +26,6 @@ import sys import warnings -from qdk import TargetProfile -from qdk import init as qdk_init -from qsharp._qsharp import get_config as get_qdk_profile_config - # Import some tools for convenience import qdk_chemistry.constants from qdk_chemistry._core import QDKChemistryConfig @@ -41,19 +37,6 @@ _DOCS_MODE = os.getenv("QDK_CHEMISTRY_DOCS", "0") == "1" -# Initialize Q# interpreter -qdk_config = get_qdk_profile_config() -_QDK_INTERPRETER_PROFILE = qdk_config.get_target_profile() -if _QDK_INTERPRETER_PROFILE == "unrestricted": # Default by Q# if not set - qdk_init(target_profile=TargetProfile.Base) - new_config = get_qdk_profile_config() - _QDK_INTERPRETER_PROFILE = new_config.get_target_profile() - Logger.debug( - f"QDK interpreter profile initialized to '{_QDK_INTERPRETER_PROFILE}'. " - "If you imported Q# code before this module was loaded, please re-import it, " - "or set your target profile before importing qdk_chemistry." - ) - def _setup_resources() -> None: """Set the QDKChemistryConfig resources directory using the runtime helper. diff --git a/python/src/qdk_chemistry/utils/qsharp/__init__.py b/python/src/qdk_chemistry/utils/qsharp/__init__.py index 533106de5..9d907f5ae 100644 --- a/python/src/qdk_chemistry/utils/qsharp/__init__.py +++ b/python/src/qdk_chemistry/utils/qsharp/__init__.py @@ -18,7 +18,7 @@ # Initialize Q# interpreter qdk_config = get_qdk_profile_config() _QDK_INTERPRETER_PROFILE = qdk_config.get_target_profile() -if _QDK_INTERPRETER_PROFILE != "adaptive_rif": # Default by Q# if not set +if _QDK_INTERPRETER_PROFILE != "adaptive_rif": target_profile = qsharp.TargetProfile.Adaptive_RIF _QDK_INTERPRETER_PROFILE = str(target_profile) Logger.debug( From eab38f09ace03cb296f45e0207598b8eeb4326fd Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Tue, 7 Apr 2026 16:15:09 +0000 Subject: [PATCH 06/21] fix qdk interpreter test --- .../qdk_chemistry/utils/qsharp/__init__.py | 16 +++++++-------- python/tests/test_qdk_interpreter_init.py | 20 +++++-------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/python/src/qdk_chemistry/utils/qsharp/__init__.py b/python/src/qdk_chemistry/utils/qsharp/__init__.py index 9d907f5ae..22cc0be96 100644 --- a/python/src/qdk_chemistry/utils/qsharp/__init__.py +++ b/python/src/qdk_chemistry/utils/qsharp/__init__.py @@ -15,18 +15,18 @@ __all__ = ["QSHARP_UTILS"] -# Initialize Q# interpreter +_QS_DIR = Path(__file__).parent + +# Ensure the Q# interpreter uses Adaptive_RIF qdk_config = get_qdk_profile_config() -_QDK_INTERPRETER_PROFILE = qdk_config.get_target_profile() -if _QDK_INTERPRETER_PROFILE != "adaptive_rif": - target_profile = qsharp.TargetProfile.Adaptive_RIF - _QDK_INTERPRETER_PROFILE = str(target_profile) +_target_profile = qsharp.TargetProfile.Adaptive_RIF + +if qdk_config.get_target_profile() != "adaptive_rif": Logger.debug( - f"QDK interpreter profile initialized to '{_QDK_INTERPRETER_PROFILE}'. " + f"QDK interpreter profile set to '{_target_profile}'. " "If you imported Q# code before this module was loaded, please re-import it, " "or set your target profile before importing qdk_chemistry." ) -_QS_DIR = Path(__file__).parent -qdk_init(project_root=_QS_DIR, target_profile=target_profile) +qdk_init(project_root=_QS_DIR, target_profile=_target_profile) QSHARP_UTILS = qdk.code.QDKChemistry.Utils diff --git a/python/tests/test_qdk_interpreter_init.py b/python/tests/test_qdk_interpreter_init.py index 3ba93db27..ab82cb3da 100644 --- a/python/tests/test_qdk_interpreter_init.py +++ b/python/tests/test_qdk_interpreter_init.py @@ -11,25 +11,15 @@ from qsharp._qsharp import get_config -def test_default_qdk_interpreter_init(): - sys.modules.pop("qdk_chemistry", None) - from qdk_chemistry import _QDK_INTERPRETER_PROFILE # noqa: PLC0415 - - assert _QDK_INTERPRETER_PROFILE == "base" - - def test_qdk_interpreter_init_with_target_profile(): - sys.modules.pop("qdk_chemistry", None) - init(target_profile=TargetProfile.Adaptive_RIF) - user_profile = get_config().get_target_profile() - assert user_profile == "adaptive_rif" - - from qdk_chemistry import _QDK_INTERPRETER_PROFILE # noqa: PLC0415 - - assert user_profile == _QDK_INTERPRETER_PROFILE + # Remove qsharp init module from cache to force re-execution of its __init__.py + sys.modules.pop("qdk_chemistry.utils.qsharp", None) init(target_profile=TargetProfile.Base) + user_profile = get_config().get_target_profile() + assert user_profile == "base" from qdk_chemistry.utils.qsharp import QSHARP_UTILS # noqa: PLC0415 + assert get_config().get_target_profile() == "adaptive_rif" assert getattr(QSHARP_UTILS, "StatePreparation", None) is not None From 12f40e37bd8bff8543a2cebfdf7d6bf0c504d12b Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Tue, 7 Apr 2026 20:01:29 +0000 Subject: [PATCH 07/21] merge staircase functionalities --- .../state_preparation/sparse_isometry.py | 195 ++++------------- .../sparse_isometry_binary_encoding.py | 2 +- .../qdk_chemistry/utils/binary_encoding.py | 97 ++++----- python/tests/test_helpers.py | 154 +++++++++++++ python/tests/test_state_preparation.py | 155 +------------ .../test_state_preparation_binary_encoding.py | 76 +------ python/tests/test_utils_binary_encoding.py | 204 ++++++++++++------ 7 files changed, 375 insertions(+), 508 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index 615366e6c..2ae39fece 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -496,7 +496,7 @@ def gf2x_with_tracking( matrix: np.ndarray, *, skip_diagonal_reduction: bool = False, - staircase_mode: bool = False, + forward_only: bool = False, ) -> GF2XEliminationResult: """Perform enhanced GF2+X Gaussian elimination with smart preprocessing and X operations. @@ -516,13 +516,8 @@ def gf2x_with_tracking( staircase rank reduction (step 4). Binary encoding handles the identity pivot block natively, so the extra CX + X expansion ops produced by the diagonal reduction are redundant Cliffords. - staircase_mode: If True, perform forward-only GF2 elimination (REF) - then convert the pivot block directly to upper-staircase form - with minimal CX ops — skipping back-substitution entirely. - This produces a matrix that binary encoding's diagonal path - recognises as ``is_diagonal_reduced``, saving ``r-1`` CX gates - vs. the full RREF + staircase cascade. Implies - ``skip_diagonal_reduction=True``. + forward_only: If True, perform forward-only GF2 elimination into row echelon form (REF), + skipping back-substitution entirely. Returns: A dataclass containing GF2+X elimination results. @@ -562,52 +557,28 @@ def gf2x_with_tracking( # Step 3: Perform standard Gaussian elimination on the remaining matrix if matrix_work.shape[0] > 0: # Only if there are rows left - if staircase_mode: - # Forward-only elimination → REF → direct staircase fill - matrix_processed, updated_row_map, new_cnot_ops = _perform_gaussian_elimination_forward_only( - matrix_work, row_map, [] - ) - - for target, control in new_cnot_ops: - operations.append(("cx", (target, control))) - - # Remove zero rows - matrix_reduced, reduced_row_map, rank = _remove_zero_rows(matrix_processed, updated_row_map) - - if rank > 1: - # Convert REF pivot block directly to staircase form - matrix_reduced, reduced_row_map, operations = _ref_to_staircase( - matrix_reduced, reduced_row_map, operations - ) - - gf2x_results = GF2XEliminationResult( - reduced_matrix=matrix_reduced, - row_map=reduced_row_map, - col_map=col_map, - operations=operations, - rank=rank, - ) - else: - matrix_processed, updated_row_map, new_cnot_ops = _perform_gaussian_elimination(matrix_work, row_map, []) + matrix_processed, updated_row_map, new_cnot_ops = _perform_gaussian_elimination( + matrix_work, row_map, [], forward_only=forward_only + ) - for target, control in new_cnot_ops: - operations.append(("cx", (target, control))) + for target, control in new_cnot_ops: + operations.append(("cx", (target, control))) - # Remove zero rows and update row_map accordingly - matrix_reduced, reduced_row_map, rank = _remove_zero_rows(matrix_processed, updated_row_map) + # Remove zero rows and update row_map accordingly + matrix_reduced, reduced_row_map, rank = _remove_zero_rows(matrix_processed, updated_row_map) - gf2x_results = GF2XEliminationResult( - reduced_matrix=matrix_reduced, - row_map=reduced_row_map, - col_map=col_map, - operations=operations, - rank=rank, - ) + gf2x_results = GF2XEliminationResult( + reduced_matrix=matrix_reduced, + row_map=reduced_row_map, + col_map=col_map, + operations=operations, + rank=rank, + ) - # Step 4: Check for diagonal matrix and apply further reduction if possible - if not skip_diagonal_reduction and rank > 1 and _is_diagonal_matrix(matrix_reduced): - Logger.info(f"Detected diagonal matrix with rank {rank}, applying further reduction") - gf2x_results = _reduce_diagonal_matrix(matrix_reduced, reduced_row_map, col_map, operations) + # Step 4: Check for diagonal matrix and apply further reduction if possible + if not forward_only and not skip_diagonal_reduction and rank > 1 and _is_diagonal_matrix(matrix_reduced): + Logger.info(f"Detected diagonal matrix with rank {rank}, applying further reduction") + gf2x_results = _reduce_diagonal_matrix(matrix_reduced, reduced_row_map, col_map, operations) # Log the final reduced matrix rank Logger.info(f"Final reduced matrix rank: {gf2x_results.rank}") @@ -739,13 +710,18 @@ def _perform_gaussian_elimination( matrix: np.ndarray, row_map: list[int], cnot_ops: list[tuple[int, int]], + *, + forward_only: bool = False, ) -> tuple[np.ndarray, list[int], list[tuple[int, int]]]: - """Perform full GF2 Gaussian elimination (forward + back-substitution). + """Perform GF2 Gaussian elimination. Args: matrix: Binary matrix to reduce (copied internally). row_map: Current-to-original row index mapping (copied internally). cnot_ops: Existing CNOT operation list (copied internally). + forward_only: If True, perform forward-only elimination into + row echelon form (REF), skipping back-substitution. If + False (default), perform full elimination into RREF. Returns: ``(reduced_matrix, updated_row_map, updated_cnot_ops)`` @@ -766,7 +742,7 @@ def _perform_gaussian_elimination( matrix_work[[pivot_row, sel]] = matrix_work[[sel, pivot_row]] row_map_work[pivot_row], row_map_work[sel] = row_map_work[sel], row_map_work[pivot_row] - _eliminate_column(matrix_work, pivot_row, col, row_map_work, cnot_ops_work) + _eliminate_column(matrix_work, pivot_row, col, row_map_work, cnot_ops_work, forward_only=forward_only) pivot_row += 1 if pivot_row == num_rows: @@ -798,8 +774,10 @@ def _eliminate_column( col: int, row_map: list[int], cnot_ops: list[tuple[int, int]], + *, + forward_only: bool = False, ) -> None: - """Eliminate all other rows in ``col`` using XOR with the pivot row. + """Eliminate rows in ``col`` using XOR with the pivot row. Modifies ``matrix`` and ``cnot_ops`` **in place**. @@ -809,10 +787,16 @@ def _eliminate_column( col: Column to eliminate. row_map: Current-to-original row index mapping (read-only). cnot_ops: Destination list for recorded CNOT operations. + forward_only: If True, only eliminate rows below the pivot + (forward elimination / REF). If False, eliminate all + other rows (full back-substitution / RREF). """ - targets = np.flatnonzero(matrix[:, col]) - targets = targets[targets != pivot_row] + if forward_only: + targets = np.flatnonzero(matrix[pivot_row + 1 :, col]) + pivot_row + 1 + else: + targets = np.flatnonzero(matrix[:, col]) + targets = targets[targets != pivot_row] for r in targets: matrix[r] ^= matrix[pivot_row] cnot_ops.append((row_map[r], row_map[pivot_row])) @@ -838,107 +822,6 @@ def _remove_zero_rows(matrix: np.ndarray, row_map: list[int]) -> tuple[np.ndarra ) -def _perform_gaussian_elimination_forward_only( - matrix: np.ndarray, - row_map: list[int], - cnot_ops: list[tuple[int, int]], -) -> tuple[np.ndarray, list[int], list[tuple[int, int]]]: - """Perform forward-only GF2 Gaussian elimination (no back-substitution). - - Produces an upper-triangular (row echelon form) matrix rather than RREF. - Back-substitution is skipped so that binary encoding's staircase - conversion can be applied directly to the REF pivot block. - - Args: - matrix: Binary matrix to reduce (copied internally). - row_map: Current-to-original row index mapping (copied internally). - cnot_ops: Existing CNOT operation list (copied internally). - - Returns: - ``(reduced_matrix, updated_row_map, updated_cnot_ops)`` - - """ - matrix_work = matrix.copy() - row_map_work = row_map.copy() - cnot_ops_work = cnot_ops.copy() - num_rows, num_cols = matrix_work.shape - - pivot_row = 0 - for col in range(num_cols): - sel = _find_pivot_row(matrix_work, pivot_row, col) - if sel is None: - continue - - if sel != pivot_row: - matrix_work[[pivot_row, sel]] = matrix_work[[sel, pivot_row]] - row_map_work[pivot_row], row_map_work[sel] = row_map_work[sel], row_map_work[pivot_row] - - # Eliminate only rows BELOW the pivot (forward elimination only) - below = np.flatnonzero(matrix_work[pivot_row + 1 :, col]) + pivot_row + 1 - for r in below: - matrix_work[r] ^= matrix_work[pivot_row] - cnot_ops_work.append((row_map_work[r], row_map_work[pivot_row])) - - pivot_row += 1 - if pivot_row == num_rows: - break - - return matrix_work, row_map_work, cnot_ops_work - - -def _ref_to_staircase( - matrix: np.ndarray, - row_map: list[int], - operations: list[tuple[str, int | tuple[int, int]]], -) -> tuple[np.ndarray, list[int], list[tuple[str, int | tuple[int, int]]]]: - """Convert a REF (upper-triangular) pivot block to upper-staircase form. - - For each above-diagonal entry ``(i, pivot_col_j)`` where ``j > i``: - - * If the entry is already 1, it's correct — do nothing. - * If the entry is 0, apply CX(control=row_j, target=row_i) to set it to 1. - - Processing columns left-to-right guarantees that side effects on later - columns are absorbed when we reach them. - - After this transform the pivot sub-matrix equals ``np.triu(ones(r, r))`` - which binary encoding's ``_is_diagonal_reduction_shape`` recognises, - allowing it to skip its own CX cascade. - - Args: - matrix: REF binary matrix (rank x n_cols). - row_map: Current row-to-original-qubit mapping. - operations: Existing operations list to extend. - - Returns: - ``(matrix, row_map, operations)`` — updated in place. - - """ - matrix_work = matrix.copy() - row_map_work = row_map.copy() - operations_work = operations.copy() - - rank = matrix_work.shape[0] - - # Identify pivot columns - pivot_cols: list[int] = [] - for r in range(rank): - nz = np.flatnonzero(matrix_work[r]) - if nz.size > 0: - pivot_cols.append(int(nz[0])) - - # Fill above-diagonal entries in pivot columns to reach staircase form - for j_idx in range(1, len(pivot_cols)): - pc = pivot_cols[j_idx] - for i_idx in range(j_idx): - if not matrix_work[i_idx, pc]: - # Need to set this entry to 1: CX(control=row_j, target=row_i) - matrix_work[i_idx] ^= matrix_work[j_idx] - operations_work.append(("cx", (row_map_work[i_idx], row_map_work[j_idx]))) - - return matrix_work, row_map_work, operations_work - - def _reduce_diagonal_matrix( matrix: np.ndarray, row_map: list[int], diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index 4ed977835..e59f88731 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -119,7 +119,7 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: # binary encoding's stage-1 handles the identity pivot block natively; # the extra CX + X ops from diagonal reduction would be redundant. bitstring_matrix = self._bitstrings_to_binary_matrix(bitstrings) - gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, staircase_mode=True) + gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, forward_only=True) # Step 2: Binary encoding on the reduced RREF matrix binary_ops, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index f4572494c..caef3f0d6 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -30,8 +30,8 @@ __all__ = [ "BinaryEncodingSynthesizer", "MatrixCompressionType", - "NotRrefError", - "RrefTableau", + "NotRefError", + "RefTableau", ] @@ -76,8 +76,8 @@ def _bits_to_int(bits: Iterable[int | bool]) -> int: return sum(int(b) << i for i, b in enumerate(reversed(list(bits)))) -class NotRrefError(ValueError): - """Raised when a matrix is not in reduced row echelon form (RREF).""" +class NotRefError(ValueError): + """Raised when a matrix is not in row echelon form (REF).""" class MatrixCompressionType(Enum): @@ -90,43 +90,18 @@ class MatrixCompressionType(Enum): X = "x" -def _is_diagonal_reduction_shape(data: np.ndarray) -> bool: - """Return True when the pivot sub-matrix is already upper-staircase. +def _check_ref(data: np.ndarray) -> None: + """Validate that a binary matrix is in row echelon form (REF). - Args: - data: Binary matrix to check. - - Returns: - True if the pivot sub-matrix is upper-triangular with all 1s, False otherwise. - - """ - row_norms = np.any(data, axis=1) - effective_rows = int(row_norms.sum()) if row_norms.any() else 0 - if effective_rows == 0: - return False - - # Identify pivot columns (leftmost 1 in each non-zero row) - pivot_cols: list[int] = [] - for r in range(effective_rows): - nz = np.flatnonzero(data[r]) - if nz.size == 0: - return False - pivot_cols.append(int(nz[0])) - - # Check that pivot sub-matrix is upper-triangular with all 1s - pivot_submatrix = data[np.ix_(range(effective_rows), pivot_cols)] - expected = np.triu(np.ones((effective_rows, effective_rows), dtype=np.int8)) - return bool(np.array_equal(pivot_submatrix, expected)) - - -def _check_rref(data: np.ndarray) -> None: - """Validate that a binary matrix is in reduced row echelon form. + REF requires non-zero rows to appear before any all-zero rows and each + row's leading 1 (pivot) to be strictly to the right of the pivot above. + Unlike RREF, pivot columns may have multiple non-zero entries. Args: data: Binary matrix to validate. Raises: - NotRrefError: If *data* is not in RREF. + NotRefError: If *data* is not in REF. """ num_rows, _ = data.shape @@ -138,24 +113,21 @@ def _check_rref(data: np.ndarray) -> None: found_zero_row = True continue if found_zero_row: - raise NotRrefError(f"Non-zero row {row} appears after an all-zero row") + raise NotRefError(f"Non-zero row {row} appears after an all-zero row") pivot_col = int(nz[0]) if pivot_col <= prev_pivot: - raise NotRrefError( + raise NotRefError( f"Pivot at row {row}, col {pivot_col} is not strictly to the right of previous pivot col {prev_pivot}" ) - col_sum = int(data[:, pivot_col].sum()) - if col_sum != 1: - raise NotRrefError(f"Pivot column {pivot_col} has {col_sum} non-zero entries (expected 1)") prev_pivot = pivot_col -class RrefTableau: +class RefTableau: """Binary tableau for the batched sparse-isometry algorithm. - The input matrix must be in reduced row echelon form (RREF) or - upper-staircase diagonal-reduction shape. + The input matrix must be in row echelon form (REF), reduced row echelon + form (RREF), or upper-staircase diagonal-reduction shape. The tableau supports in-place updates via the compression operations. """ @@ -168,7 +140,7 @@ def __init__(self, data: np.ndarray): basis states. Values are coerced to ``np.int8``. Raises: - NotRrefError: If ``data`` is neither in RREF nor in the accepted + NotRefError: If ``data`` is not in REF, RREF, or the accepted upper-staircase diagonal-reduction form. AssertionError: If the matrix rank/size assumptions required by the algorithm are violated. @@ -177,9 +149,7 @@ def __init__(self, data: np.ndarray): self.data = np.asarray(data, dtype=np.int8) assert self.data.ndim == 2 - self.is_diagonal_reduced = _is_diagonal_reduction_shape(self.data) - if not self.is_diagonal_reduced: - _check_rref(self.data) + _check_ref(self.data) self.num_rows, self.num_cols = self.data.shape self.dense_size = _dense_qubits_size(self.num_cols) @@ -365,7 +335,7 @@ class BinaryEncodingSynthesizer: def __init__( self, - tableau: RrefTableau, + tableau: RefTableau, *, include_negative_controls: bool = True, measurement_based_uncompute: bool = False, @@ -408,13 +378,13 @@ def from_matrix( ) -> BinaryEncodingSynthesizer: """Create a synthesiser, run both stages, and return the solved instance. - This is the primary entry point. It validates the input RREF matrix, + This is the primary entry point. It validates the input matrix, executes the full two-stage synthesis, and returns the ready-to-export synthesiser. Args: - matrix: Binary (0/1) matrix in RREF or upper-staircase diagonal - form, shaped ``(num_qubits, num_determinants)``. + matrix: Binary (0/1) matrix in REF, RREF, or upper-staircase + diagonal form, shaped ``(num_qubits, num_determinants)``. include_negative_controls: If True, include both positive and negative (0-valued) fixed controls in PUI blocks. If False, only positive (1-valued) controls are emitted. @@ -426,11 +396,11 @@ def from_matrix( A solved :class:`BinaryEncodingSynthesizer`. Raises: - NotRrefError: If *matrix* is not in valid RREF form. + NotRefError: If *matrix* is not in valid REF form. """ synth = cls( - RrefTableau(matrix), + RefTableau(matrix), include_negative_controls=include_negative_controls, measurement_based_uncompute=measurement_based_uncompute, ) @@ -554,22 +524,29 @@ def _run_stage1_diagonal_encoding(self, rank: int): self.bijection.append((dense_val, c)) def _apply_unary_staircase(self, rank: int) -> list[int]: - """Convert the identity block into an upper-staircase matrix. + """Convert the pivot block into an upper-staircase matrix. - For each pivot column c, apply CX from row c into every row above it, - then apply X to row 0 and SWAP to move the pivot row up to row c. + Inspects each above-diagonal entry in the pivot block (columns + 0 … rank-1 after pivot permutation) and emits a CX to fill any + missing 1. This handles RREF (identity), REF (upper-triangular), + and staircase (already filled) inputs uniformly. + + Processing columns left-to-right ensures side effects on later + columns are absorbed when they are reached. Args: - rank: Number of pivot columns (size of the identity block). + rank: Number of pivot columns (size of the pivot block). Returns: List of logical row indices corresponding to the original pivot rows. """ logical_rows = list(range(rank)) - if not self.tableau.is_diagonal_reduced: - for i in range(rank - 2, -1, -1): - self._record((MatrixCompressionType.CX, (logical_rows[i + 1], logical_rows[i]))) + # Fill above-diagonal 0s in the pivot block to reach upper-staircase + for j in range(1, rank): + for i in range(j): + if not self.tableau.data[logical_rows[i], j]: + self._record((MatrixCompressionType.CX, (logical_rows[j], logical_rows[i]))) self._record((MatrixCompressionType.X, (logical_rows[0],))) return logical_rows diff --git a/python/tests/test_helpers.py b/python/tests/test_helpers.py index ec786ee54..2a7627acb 100644 --- a/python/tests/test_helpers.py +++ b/python/tests/test_helpers.py @@ -247,3 +247,157 @@ def create_test_ansatz(num_orbitals: int = 2): wavefunction = Wavefunction(container) return Ansatz(hamiltonian, wavefunction) + + +def _hf_determinant(n_alpha: int, n_beta: int, n_orbitals: int) -> np.ndarray: + """Build the Hartree-Fock reference determinant [alpha|beta]. + + Args: + n_alpha: Number of alpha electrons. + n_beta: Number of beta electrons. + n_orbitals: Number of spatial orbitals. + + Returns: + Occupation array of shape ``(2 * n_orbitals,)`` with 1s for occupied + alpha/beta orbitals and 0s elsewhere. + + """ + det = np.zeros(2 * n_orbitals, dtype=np.int8) + det[:n_alpha] = 1 + det[n_orbitals : n_orbitals + n_beta] = 1 + return det + + +def _random_excitation(det: np.ndarray, n_orbitals: int, rng: np.random.Generator) -> np.ndarray | None: + """Apply a random excitation independently in alpha and beta channels. + + Args: + det: Base determinant occupation array. + n_orbitals: Number of spatial orbitals. + rng: NumPy random generator. + + Returns: + New determinant array, or ``None`` if no excitation was applied. + + """ + new_det = det.copy() + for channel_start in (0, n_orbitals): + channel = det[channel_start : channel_start + n_orbitals] + occupied = np.where(channel == 1)[0] + virtual = np.where(channel == 0)[0] + if len(occupied) == 0 or len(virtual) == 0: + continue + order = rng.integers(0, min(len(occupied), len(virtual)) + 1) + if order == 0: + continue + occ = rng.choice(occupied, size=order, replace=False) + vir = rng.choice(virtual, size=order, replace=False) + new_det[channel_start + occ] = 0 + new_det[channel_start + vir] = 1 + return None if np.array_equal(new_det, det) else new_det + + +def _generate_determinant_matrix( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, +) -> np.ndarray: + """Generate a determinant occupation matrix from HF + random excitations. + + Builds the Hartree-Fock reference determinant and applies random + single/double excitations to produce ``n_dets`` distinct determinants. + + Args: + n_electrons: Total number of electrons (split equally between alpha/beta). + n_orbitals: Number of spatial orbitals. + n_dets: Target number of determinants. + seed: Random seed for reproducibility. + + Returns: + Occupation matrix of shape ``(n_dets, 2 * n_orbitals)`` where each row + is a determinant with alpha and beta occupation blocks. + + """ + n_alpha = n_electrons // 2 + n_beta = n_electrons - n_alpha + rng = np.random.default_rng(seed) + hf = _hf_determinant(n_alpha, n_beta, n_orbitals) + + seen: set[bytes] = {hf.tobytes()} + dets = [hf] + for _ in range(n_dets * 200): + if len(dets) >= n_dets: + break + exc = _random_excitation(hf, n_orbitals, rng) + if exc is not None and exc.tobytes() not in seen: + seen.add(exc.tobytes()) + dets.append(exc) + + return np.array(dets, dtype=np.int8) + + +def create_random_bitstring_matrix( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, +) -> np.ndarray: + """Generate a random bitstring matrix suitable for GF2+X elimination. + + Builds physically meaningful determinants from the Hartree-Fock reference + plus random excitations, then transposes to the binary matrix form + expected by ``gf2x_with_tracking`` and ``BinaryEncodingSynthesizer``. + + Args: + n_electrons: Total number of electrons. + n_orbitals: Number of spatial orbitals. + n_dets: Target number of determinants (columns). + seed: Random seed for reproducibility. + + Returns: + Binary matrix of shape ``(2 * n_orbitals, n_dets)`` where rows are + qubits and columns are determinants. + + """ + det_matrix = _generate_determinant_matrix(n_electrons, n_orbitals, n_dets, seed) + return det_matrix.T + + +def create_random_wavefunction( + n_electrons: int, + n_orbitals: int, + n_dets: int, + seed: int = 0, +) -> Wavefunction: + """Generate a random normalised Wavefunction for testing. + + Builds physically meaningful determinants from the Hartree-Fock reference + plus random excitations, assigns random normalised coefficients, and wraps + them in a :class:`Wavefunction`. + + Args: + n_electrons: Total number of electrons. + n_orbitals: Number of spatial orbitals. + n_dets: Target number of determinants. + seed: Random seed for reproducibility. + + Returns: + A normalised :class:`Wavefunction` with ``n_dets`` determinants. + + """ + det_matrix = _generate_determinant_matrix(n_electrons, n_orbitals, n_dets, seed) + actual_n_dets = det_matrix.shape[0] + + mapping = {(1, 1): "2", (1, 0): "u", (0, 1): "d", (0, 0): "0"} + configs = [ + Configuration("".join(mapping[int(row[i]), int(row[n_orbitals + i])] for i in range(n_orbitals))) + for row in det_matrix + ] + + coeff_rng = np.random.default_rng(seed) + raw = coeff_rng.standard_normal(actual_n_dets) + coeffs = raw / np.linalg.norm(raw) + + orbitals = create_test_orbitals(n_orbitals) + return Wavefunction(CasWavefunctionContainer(coeffs, configs, orbitals)) diff --git a/python/tests/test_state_preparation.py b/python/tests/test_state_preparation.py index 55b9818b6..a73517701 100644 --- a/python/tests/test_state_preparation.py +++ b/python/tests/test_state_preparation.py @@ -28,9 +28,7 @@ _find_pivot_row, _is_diagonal_matrix, _perform_gaussian_elimination, - _perform_gaussian_elimination_forward_only, _reduce_diagonal_matrix, - _ref_to_staircase, _remove_all_ones_rows_with_x, _remove_duplicate_rows_with_cnot, _remove_zero_rows, @@ -960,7 +958,7 @@ def test_gf2x_with_tracking_edge_case_pseudo_diagonal(): def test_forward_only_produces_upper_triangular(): """Forward-only elimination produces row echelon form (zeros below each pivot).""" matrix = np.array([[1, 1, 0], [1, 0, 1], [0, 1, 1]], dtype=np.int8) - m_result, _, _ = _perform_gaussian_elimination_forward_only(matrix, [0, 1, 2], []) + m_result, _, _ = _perform_gaussian_elimination(matrix, [0, 1, 2], [], forward_only=True) for r in range(m_result.shape[0]): nz = np.flatnonzero(m_result[r]) @@ -973,7 +971,7 @@ def test_forward_only_produces_upper_triangular(): def test_forward_only_does_not_back_substitute(): """Forward-only differs from full RREF — above-diagonal entries survive.""" matrix = np.array([[1, 1, 0], [0, 1, 1], [1, 0, 1]], dtype=np.int8) - m_fwd, _, _ = _perform_gaussian_elimination_forward_only(matrix, [0, 1, 2], []) + m_fwd, _, _ = _perform_gaussian_elimination(matrix, [0, 1, 2], [], forward_only=True) m_full, _, _ = _perform_gaussian_elimination(matrix, [0, 1, 2], []) assert not np.array_equal(m_fwd, m_full) @@ -985,7 +983,7 @@ def test_forward_only_reconstruction(): dtype=np.int8, ) row_map = list(range(4)) - m_result, rm_result, cnot_ops = _perform_gaussian_elimination_forward_only(matrix, row_map, []) + m_result, rm_result, cnot_ops = _perform_gaussian_elimination(matrix, row_map, [], forward_only=True) reconstructed = np.zeros_like(matrix) for i, orig in enumerate(rm_result): @@ -999,7 +997,7 @@ def test_forward_only_reconstruction(): def test_forward_only_row_swap_tracking(): """Row swaps needed for pivoting are reflected in the returned row_map.""" matrix = np.array([[0, 1], [1, 0]], dtype=np.int8) - _, rm_result, _ = _perform_gaussian_elimination_forward_only(matrix, [0, 1], []) + _, rm_result, _ = _perform_gaussian_elimination(matrix, [0, 1], [], forward_only=True) assert rm_result[0] == 1 assert rm_result[1] == 0 @@ -1017,7 +1015,7 @@ def test_forward_only_large_rank_deficient(): ], dtype=np.int8, ) - m_ref, rm, ops = _perform_gaussian_elimination_forward_only(matrix, list(range(6)), []) + m_ref, rm, ops = _perform_gaussian_elimination(matrix, list(range(6)), [], forward_only=True) non_zero = int(np.sum(np.any(m_ref, axis=1))) assert non_zero == 4 @@ -1047,151 +1045,10 @@ def test_forward_only_wide_matrix(): ], dtype=np.int8, ) - m_ref, _, ops = _perform_gaussian_elimination_forward_only(matrix, list(range(3)), []) + m_ref, _, ops = _perform_gaussian_elimination(matrix, list(range(3)), [], forward_only=True) # Already in REF (identity-like left block), should need 0 ops assert len(ops) == 0 assert np.array_equal(m_ref, matrix) # All 3 rows non-zero assert int(np.sum(np.any(m_ref, axis=1))) == 3 - - -def test_ref_to_staircase_fills_above_diagonal(): - """Every above-diagonal entry in a pivot column becomes 1.""" - matrix = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 1]], dtype=np.int8) - m_result, _, _ = _ref_to_staircase(matrix, [0, 1, 2], []) - - pivot_cols = [int(np.flatnonzero(m_result[r])[0]) for r in range(3)] - for j_idx, pc in enumerate(pivot_cols): - for i_idx in range(j_idx): - assert m_result[i_idx, pc] == 1 - - -def test_ref_to_staircase_already_done_no_ops(): - """If the matrix is already staircase, no CX ops are emitted.""" - staircase = np.array([[1, 1, 1], [0, 1, 1], [0, 0, 1]], dtype=np.int8) - _, _, ops = _ref_to_staircase(staircase, [0, 1, 2], []) - assert [op for op in ops if op[0] == "cx"] == [] - - -def test_ref_to_staircase_reconstruction(): - """Reversing the CX ops on the staircase result recovers the input REF.""" - ref_matrix = np.array( - [[1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 0]], - dtype=np.int8, - ) - row_map = [2, 5, 8] - m_result, rm_result, ops = _ref_to_staircase(ref_matrix, row_map, []) - - reconstructed = m_result.copy() - for op_name, args in reversed(ops): - if op_name == "cx": - target, control = args - reconstructed[rm_result.index(target)] ^= reconstructed[rm_result.index(control)] - - assert np.array_equal(reconstructed, ref_matrix) - - -def test_ref_to_staircase_5x8_complex(): - """5-row REF with non-contiguous pivot columns: staircase fills all above-diagonal pivot entries.""" - # Pivots at columns 0, 2, 3, 5, 7 — gaps at cols 1, 4, 6. - ref = np.array( - [ - [1, 1, 0, 0, 1, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 1, 0], - [0, 0, 0, 1, 1, 0, 0, 1], - [0, 0, 0, 0, 0, 1, 0, 1], - [0, 0, 0, 0, 0, 0, 0, 1], - ], - dtype=np.int8, - ) - # Verify it's valid REF before transforming - for r in range(ref.shape[0]): - nz = np.flatnonzero(ref[r]) - if nz.size: - assert np.all(ref[r + 1 :, int(nz[0])] == 0) - - row_map = [0, 3, 5, 7, 9] - m_result, rm_result, ops = _ref_to_staircase(ref, row_map, []) - - # Check staircase property: all above-diagonal pivot entries are 1 - pivot_cols = [] - for r in range(m_result.shape[0]): - nz = np.flatnonzero(m_result[r]) - if nz.size: - pivot_cols.append(int(nz[0])) - for j_idx, pc in enumerate(pivot_cols): - for i_idx in range(j_idx): - assert m_result[i_idx, pc] == 1 - - # CX ops should use the row_map indices, not matrix row indices - for op_name, args in ops: - if op_name == "cx": - assert args[0] in row_map - assert args[1] in row_map - - # Reconstruction: undo CX ops to recover original REF - reconstructed = m_result.copy() - for op_name, args in reversed(ops): - if op_name == "cx": - target, control = args - reconstructed[rm_result.index(target)] ^= reconstructed[rm_result.index(control)] - assert np.array_equal(reconstructed, ref) - - -def test_forward_only_then_staircase_end_to_end(): - """Full pipeline: raw matrix, forward-only REF, staircase, verify properties.""" - # A realistic-ish 5x6 binary matrix (5 qubits, 6 determinants) - matrix = np.array( - [ - [1, 0, 1, 1, 0, 1], - [0, 1, 1, 0, 1, 1], - [1, 1, 0, 0, 1, 0], - [0, 0, 1, 1, 1, 0], - [1, 0, 0, 1, 0, 0], - ], - dtype=np.int8, - ) - row_map = list(range(5)) - - # Step 1: forward-only to REF - m_ref, rm_ref, cnot_ops = _perform_gaussian_elimination_forward_only(matrix, row_map, []) - - # Verify REF - for r in range(m_ref.shape[0]): - nz = np.flatnonzero(m_ref[r]) - if nz.size == 0: - continue - assert np.all(m_ref[r + 1 :, int(nz[0])] == 0) - - # Remove zero rows - non_zero_mask = np.any(m_ref, axis=1) - ref_nz = m_ref[non_zero_mask] - rm_nz = [rm_ref[i] for i in range(len(rm_ref)) if non_zero_mask[i]] - - # Step 2: staircase conversion - ops_so_far = [("cx", (t, c)) for t, c in cnot_ops] - m_stair, rm_stair, all_ops = _ref_to_staircase(ref_nz, rm_nz, ops_so_far) - - # Verify staircase: every above-diagonal pivot entry is 1 - pivot_cols = [] - for r in range(m_stair.shape[0]): - nz = np.flatnonzero(m_stair[r]) - if nz.size: - pivot_cols.append(int(nz[0])) - for j_idx, pc in enumerate(pivot_cols): - for i_idx in range(j_idx): - assert m_stair[i_idx, pc] == 1 - - # Verify combined reconstruction recovers original matrix - reconstructed = np.zeros_like(matrix) - for i, orig in enumerate(rm_stair): - reconstructed[orig] = m_stair[i] - - # Undo all ops in reverse (staircase CX ops first, then forward-elim CX ops) - for op_name, args in reversed(all_ops): - if op_name == "cx": - target, control = args - reconstructed[target] ^= reconstructed[control] - - assert np.array_equal(reconstructed, matrix) diff --git a/python/tests/test_state_preparation_binary_encoding.py b/python/tests/test_state_preparation_binary_encoding.py index 30ccb61b2..2f1207e65 100644 --- a/python/tests/test_state_preparation_binary_encoding.py +++ b/python/tests/test_state_preparation_binary_encoding.py @@ -13,78 +13,12 @@ from qdk_chemistry.algorithms.state_preparation import SparseIsometryBinaryEncodingStatePreparation from qdk_chemistry.algorithms.state_preparation.sparse_isometry import gf2x_with_tracking from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import _encode_gf2x_ops_for_qs -from qdk_chemistry.data import CasWavefunctionContainer, Circuit, Configuration, Wavefunction +from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.plugins.qiskit import QDK_CHEMISTRY_HAS_QISKIT from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer from .reference_tolerances import float_comparison_absolute_tolerance, float_comparison_relative_tolerance -from .test_helpers import create_test_orbitals - - -def _hf_determinant(n_alpha: int, n_beta: int, n_orbitals: int) -> np.ndarray: - """Build the Hartree-Fock reference determinant [alpha|beta].""" - det = np.zeros(2 * n_orbitals, dtype=np.int8) - det[:n_alpha] = 1 - det[n_orbitals : n_orbitals + n_beta] = 1 - return det - - -def _random_excitation(det: np.ndarray, n_orbitals: int, rng: np.random.Generator) -> np.ndarray | None: - """Apply a random excitation independently in alpha and beta channels.""" - new_det = det.copy() - for channel_start in (0, n_orbitals): - channel = det[channel_start : channel_start + n_orbitals] - occupied = np.where(channel == 1)[0] - virtual = np.where(channel == 0)[0] - if len(occupied) == 0 or len(virtual) == 0: - continue - order = rng.integers(0, min(len(occupied), len(virtual)) + 1) - if order == 0: - continue - occ = rng.choice(occupied, size=order, replace=False) - vir = rng.choice(virtual, size=order, replace=False) - new_det[channel_start + occ] = 0 - new_det[channel_start + vir] = 1 - return None if np.array_equal(new_det, det) else new_det - - -def _determinants_to_configs(matrix: np.ndarray, n_orbitals: int) -> list[str]: - """Convert (n_dets, 2*n_orbitals) occupation matrix to config strings.""" - mapping = {(1, 1): "2", (1, 0): "u", (0, 1): "d", (0, 0): "0"} - return ["".join(mapping[int(row[i]), int(row[n_orbitals + i])] for i in range(n_orbitals)) for row in matrix] - - -def _generate_random_wavefunction( - n_electrons: int, - n_orbitals: int, - n_dets: int, - seed: int = 0, -) -> Wavefunction: - """Generate a random normalised Wavefunction for testing.""" - n_alpha = n_electrons // 2 - n_beta = n_electrons - n_alpha - rng = np.random.default_rng(seed) - hf = _hf_determinant(n_alpha, n_beta, n_orbitals) - - seen: set[bytes] = {hf.tobytes()} - dets = [hf] - for _ in range(n_dets * 200): - if len(dets) >= n_dets: - break - exc = _random_excitation(hf, n_orbitals, rng) - if exc is not None and exc.tobytes() not in seen: - seen.add(exc.tobytes()) - dets.append(exc) - - det_matrix = np.array(dets, dtype=np.int8) - configs = [Configuration(s) for s in _determinants_to_configs(det_matrix, n_orbitals)] - - coeff_rng = np.random.default_rng(seed) - raw = coeff_rng.standard_normal(n_dets) - coeffs = raw / np.linalg.norm(raw) - - orbitals = create_test_orbitals(n_orbitals) - return Wavefunction(CasWavefunctionContainer(coeffs, configs, orbitals)) +from .test_helpers import create_random_wavefunction def _matrix_qubit_counts(wf: Wavefunction) -> tuple[int, int]: @@ -106,7 +40,7 @@ def _matrix_qubit_counts(wf: Wavefunction) -> tuple[int, int]: n_system = len(bitstrings[0]) matrix = np.array([[int(b) for b in bs] for bs in bitstrings], dtype=np.int8).T - gf2x_result = gf2x_with_tracking(matrix, skip_diagonal_reduction=True, staircase_mode=True) + gf2x_result = gf2x_with_tracking(matrix, skip_diagonal_reduction=True, forward_only=True) synthesizer = BinaryEncodingSynthesizer.from_matrix(gf2x_result.reduced_matrix) ops = synthesizer.to_gf2x_operations( @@ -191,7 +125,7 @@ def test_random_wavefunction(self, n_electrons, n_orbitals, n_dets, seed): The expected qubit count is decomposed into system qubits (from the matrix dimensions) and ancilla qubits (from the compiled Q# circuit). """ - wf = _generate_random_wavefunction( + wf = create_random_wavefunction( n_electrons=n_electrons, n_orbitals=n_orbitals, n_dets=n_dets, @@ -247,7 +181,7 @@ def test_random_wavefunction_statevector(self, n_electrons, n_orbitals, n_dets, from qdk_chemistry.plugins.qiskit.conversion import create_statevector_from_wavefunction # noqa: PLC0415 - wf = _generate_random_wavefunction( + wf = create_random_wavefunction( n_electrons=n_electrons, n_orbitals=n_orbitals, n_dets=n_dets, diff --git a/python/tests/test_utils_binary_encoding.py b/python/tests/test_utils_binary_encoding.py index 84e51f8c3..2794de5f5 100644 --- a/python/tests/test_utils_binary_encoding.py +++ b/python/tests/test_utils_binary_encoding.py @@ -8,19 +8,21 @@ import numpy as np import pytest +from qdk_chemistry.algorithms.state_preparation.sparse_isometry import gf2x_with_tracking from qdk_chemistry.utils.binary_encoding import ( BinaryEncodingSynthesizer, MatrixCompressionType, - NotRrefError, - RrefTableau, + NotRefError, + RefTableau, _bits_to_int, - _check_rref, + _check_ref, _dense_qubits_size, _int_to_bits, - _is_diagonal_reduction_shape, _lookup_select, ) +from .test_helpers import create_random_bitstring_matrix + class TestDenseQubitsSize: """Tests for _dense_qubits_size.""" @@ -87,67 +89,49 @@ def test_roundtrip(self): assert _bits_to_int(_int_to_bits(val, 4)) == val -class TestCheckRref: - """Tests for _check_rref RREF validation.""" +class TestCheckRef: + """Tests for _check_ref REF validation.""" - def test_identity_is_rref(self): - """Identity matrix is a valid RREF.""" - _check_rref(np.eye(3, dtype=np.int8)) + def test_identity_is_ref(self): + """Identity matrix is a valid REF.""" + _check_ref(np.eye(3, dtype=np.int8)) - def test_valid_rref_non_square(self): - """Non-square matrix with trailing zero row is valid RREF.""" + def test_valid_ref_non_square(self): + """Non-square matrix with trailing zero row is valid REF.""" mat = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 0, 0]], dtype=np.int8) - _check_rref(mat) + _check_ref(mat) - def test_valid_rref_with_trailing_zeros(self): - """RREF with a trailing all-zero row is accepted.""" + def test_valid_ref_with_trailing_zeros(self): + """REF with a trailing all-zero row is accepted.""" mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) - _check_rref(mat) + _check_ref(mat) + + def test_valid_ref_upper_triangular(self): + """REF matrix with entries above the diagonal is accepted.""" + mat = np.array([[1, 1], [0, 1]], dtype=np.int8) + _check_ref(mat) def test_empty_matrix(self): - """All-zero matrix is trivially in RREF.""" - _check_rref(np.zeros((3, 3), dtype=np.int8)) + """All-zero matrix is trivially in REF.""" + _check_ref(np.zeros((3, 3), dtype=np.int8)) - def test_non_rref_pivots_not_increasing(self): + def test_non_ref_pivots_not_increasing(self): """Pivots must appear in strictly increasing column order.""" mat = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]], dtype=np.int8) - with pytest.raises(NotRrefError, match="not strictly to the right"): - _check_rref(mat) - - def test_non_rref_pivot_col_not_unique(self): - """Pivot column with two non-zero entries is rejected.""" - mat = np.array([[1, 0], [1, 1]], dtype=np.int8) - with pytest.raises(NotRrefError, match="non-zero entries"): - _check_rref(mat) + with pytest.raises(NotRefError, match="not strictly to the right"): + _check_ref(mat) - def test_non_rref_nonzero_after_zero_row(self): + def test_non_ref_nonzero_after_zero_row(self): """Non-zero row appearing after an all-zero row is rejected.""" mat = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 1]], dtype=np.int8) - with pytest.raises(NotRrefError, match="after an all-zero row"): - _check_rref(mat) - - -class TestIsDiagonalReductionShape: - """Tests for _is_diagonal_reduction_shape.""" - - def test_identity_is_not_staircase(self): - """Identity matrix has no upper-triangular fill, so it is not staircase.""" - assert not _is_diagonal_reduction_shape(np.eye(3, dtype=np.int8)) + with pytest.raises(NotRefError, match="after an all-zero row"): + _check_ref(mat) - def test_upper_triangular_ones(self): - """Upper-triangular matrix with fill above the diagonal is staircase.""" - mat = np.array([[1, 1, 1], [0, 1, 1], [0, 0, 1]], dtype=np.int8) - assert _is_diagonal_reduction_shape(mat) - def test_all_zeros(self): - """All-zero matrix is not staircase-shaped.""" - assert not _is_diagonal_reduction_shape(np.zeros((3, 3), dtype=np.int8)) +class TestRefTableau: + """Tests for RefTableau construction and gate operations.""" - -class TestRrefTableau: - """Tests for RrefTableau construction and gate operations.""" - - def _make_rref(self, n_pivots: int, n_extra_cols: int) -> RrefTableau: + def _make_ref(self, n_pivots: int, n_extra_cols: int) -> RefTableau: """Build a realistic RREF tableau with fill in non-pivot columns. The pivot block is an identity matrix. Non-pivot columns get @@ -159,7 +143,7 @@ def _make_rref(self, n_pivots: int, n_extra_cols: int) -> RrefTableau: n_extra_cols: Additional non-pivot columns to add after the pivots. Returns: - RrefTableau with the specified shape and pivot structure. + RefTableau with the specified shape and pivot structure. """ num_cols = n_pivots + n_extra_cols @@ -170,25 +154,25 @@ def _make_rref(self, n_pivots: int, n_extra_cols: int) -> RrefTableau: for c in range(n_pivots, num_cols): for r in range(n_pivots): mat[r, c] = (r + c) % 2 - return RrefTableau(mat) + return RefTableau(mat) def test_construction_from_rref(self): """Valid RREF matrix produces a tableau with correct dimensions and pivots.""" - t = self._make_rref(3, 2) + t = self._make_ref(3, 2) assert t.num_rows == 4 assert t.num_cols == 5 assert t.dense_size == _dense_qubits_size(5) assert len(t.pivots) == 3 - def test_construction_rejects_non_rref(self): - """Non-RREF matrix must raise NotRrefError.""" + def test_construction_rejects_non_ref(self): + """Non-REF matrix must raise NotRefError.""" mat = np.array([[0, 1], [1, 0]], dtype=np.int8) - with pytest.raises(NotRrefError): - RrefTableau(mat) + with pytest.raises(NotRefError): + RefTableau(mat) def test_get_and_get_col(self): """Element access and column extraction return correct values.""" - t = self._make_rref(3, 0) + t = self._make_ref(3, 0) assert t.get(0, 0) is True assert t.get(0, 1) is False col = t.get_col(1) @@ -196,54 +180,54 @@ def test_get_and_get_col(self): def test_row_is_zero(self): """Pivot rows are non-zero; trailing rows below rank are zero.""" - t = self._make_rref(3, 2) + t = self._make_ref(3, 2) assert not t.row_is_zero(0) assert not t.row_is_zero(2) assert t.row_is_zero(t.num_rows - 1) def test_cx_operation(self): """CX XORs the control row into the target row.""" - t = self._make_rref(3, 0) + t = self._make_ref(3, 0) t.cx(0, 1) np.testing.assert_array_equal(t.data[1], [1, 1, 0]) np.testing.assert_array_equal(t.data[0], [1, 0, 0]) def test_swap_operation(self): """SWAP exchanges two rows.""" - t = self._make_rref(3, 0) + t = self._make_ref(3, 0) t.swap(0, 2) np.testing.assert_array_equal(t.data[0], [0, 0, 1]) np.testing.assert_array_equal(t.data[2], [1, 0, 0]) def test_x_operation(self): """X flips every bit in the target row.""" - t = self._make_rref(3, 0) + t = self._make_ref(3, 0) t.x(0) np.testing.assert_array_equal(t.data[0], [0, 1, 1]) def test_toffoli_both_positive(self): """Toffoli with both controls positive ANDs the two rows into the target.""" mat = np.array([[1, 0, 1, 1], [0, 1, 1, 0], [0, 0, 0, 0]], dtype=np.int8) - t = RrefTableau(mat) + t = RefTableau(mat) t.toffoli(2, (0, True), (1, True)) np.testing.assert_array_equal(t.data[2], [0, 0, 1, 0]) def test_toffoli_negative_control(self): """Toffoli with a negated control ANDs row0 with ~row1 into the target.""" mat = np.array([[1, 0, 1, 1], [0, 1, 1, 0], [0, 0, 0, 0]], dtype=np.int8) - t = RrefTableau(mat) + t = RefTableau(mat) t.toffoli(2, (0, True), (1, False)) np.testing.assert_array_equal(t.data[2], [1, 0, 0, 1]) def test_identify_rref_pivots(self): """Pivot detection returns (row, col) pairs for each leading 1.""" mat = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 0, 0]], dtype=np.int8) - t = RrefTableau(mat) + t = RefTableau(mat) assert t.pivots == [(0, 0), (1, 1)] def test_permute_columns(self): """Column permutation reorders all rows accordingly.""" - t = self._make_rref(3, 0) + t = self._make_ref(3, 0) t.permute_columns([2, 1, 0]) np.testing.assert_array_equal(t.data[0], [0, 0, 1]) np.testing.assert_array_equal(t.data[2], [1, 0, 0]) @@ -259,7 +243,7 @@ def test_toffoli_pui_fixed_and_rest(self): ], dtype=np.int8, ) - t = RrefTableau(mat) + t = RefTableau(mat) # Fixed control: row 0 must be 1 t.toffoli_pui_fixed([(0, True)]) # _tmp_row should be row0 = [1, 0, 1, 0] @@ -282,10 +266,10 @@ def test_from_matrix_identity(self): assert synth.dense_size == _dense_qubits_size(4) assert len(synth.bijection) == 4 - def test_from_matrix_rejects_non_rref(self): - """Non-RREF input must raise NotRrefError.""" + def test_from_matrix_rejects_non_ref(self): + """Non-REF input must raise NotRefError.""" mat = np.array([[0, 1], [1, 0]], dtype=np.int8) - with pytest.raises(NotRrefError): + with pytest.raises(NotRefError): BinaryEncodingSynthesizer.from_matrix(mat) def test_max_batch_size_power_of_two(self): @@ -294,7 +278,7 @@ def test_max_batch_size_power_of_two(self): [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.int8, ) - synth = BinaryEncodingSynthesizer(RrefTableau(mat)) + synth = BinaryEncodingSynthesizer(RefTableau(mat)) mbs = synth.max_batch_size() assert mbs > 0 assert mbs & (mbs - 1) == 0 # power of two @@ -478,6 +462,84 @@ def test_stage1_starts_with_cx_or_x(self): first_operation_type = synth.circuit[0][0] assert first_operation_type in (MatrixCompressionType.CX, MatrixCompressionType.X) + @pytest.mark.parametrize( + ("n_electrons", "n_orbitals", "n_dets", "seed"), + [ + (6, 6, 5, 42), + (8, 10, 20, 99), + (10, 15, 30, 13), + (14, 20, 50, 0), + ], + ids=["6e6o_5det", "8e10o_20det", "10e15o_30det", "14e20o_50det"], + ) + def test_gf2x_forward_only_fewer_cx_than_rref(self, n_electrons, n_orbitals, n_dets, seed): + """Test forward_only produces fewer CX than RREF.""" + raw_matrix = create_random_bitstring_matrix( + n_electrons=n_electrons, n_orbitals=n_orbitals, n_dets=n_dets, seed=seed + ) + + # --- RREF path --- + rref_result = gf2x_with_tracking(raw_matrix, skip_diagonal_reduction=True) + rref_tableau = RefTableau(rref_result.reduced_matrix) + rref_synth = BinaryEncodingSynthesizer(rref_tableau) + rank, _ = rref_synth._permute_columns_pivots_first() + rref_synth._apply_unary_staircase(rank) + rref_cx = sum(1 for t, _ in rref_synth.circuit if t is MatrixCompressionType.CX) + assert rref_cx == rank * (rank - 1) // 2 + + # --- REF path --- + ref_result = gf2x_with_tracking(raw_matrix, forward_only=True) + ref_tableau = RefTableau(ref_result.reduced_matrix) + ref_synth = BinaryEncodingSynthesizer(ref_tableau) + rank_s, _ = ref_synth._permute_columns_pivots_first() + ref_synth._apply_unary_staircase(rank_s) + ref_cx = sum(1 for t, _ in ref_synth.circuit if t is MatrixCompressionType.CX) + assert ref_cx < rref_cx + + @pytest.mark.parametrize( + ("n_electrons", "n_orbitals", "n_dets", "seed"), + [ + (6, 6, 5, 42), + (8, 10, 20, 99), + (10, 15, 30, 13), + (14, 20, 50, 0), + ], + ids=["6e6o_5det", "8e10o_20det", "10e15o_30det", "14e20o_50det"], + ) + def test_stage1_forward_only_fewer_cx_than_rref(self, n_electrons, n_orbitals, n_dets, seed): + """Full stage 1 (staircase + binary compression) emits fewer CX for REF than RREF.""" + raw_matrix = create_random_bitstring_matrix( + n_electrons=n_electrons, n_orbitals=n_orbitals, n_dets=n_dets, seed=seed + ) + + # --- RREF path --- + rref_result = gf2x_with_tracking(raw_matrix, skip_diagonal_reduction=True) + rref_tableau = RefTableau(rref_result.reduced_matrix) + rref_synth = BinaryEncodingSynthesizer(rref_tableau) + rank, _ = rref_synth._permute_columns_pivots_first() + rref_synth._run_stage1_diagonal_encoding(rank) + rref_cx = sum(1 for t, _ in rref_synth.circuit if t is MatrixCompressionType.CX) + rref_x = sum(1 for t, _ in rref_synth.circuit if t is MatrixCompressionType.X) + + # --- REF path --- + ref_result = gf2x_with_tracking(raw_matrix, forward_only=True) + ref_tableau = RefTableau(ref_result.reduced_matrix) + ref_synth = BinaryEncodingSynthesizer(ref_tableau) + rank_s, _ = ref_synth._permute_columns_pivots_first() + ref_synth._run_stage1_diagonal_encoding(rank_s) + ref_cx = sum(1 for t, _ in ref_synth.circuit if t is MatrixCompressionType.CX) + ref_x = sum(1 for t, _ in ref_synth.circuit if t is MatrixCompressionType.X) + + assert ref_cx < rref_cx + assert ref_x == rref_x + + # After stage 1 the pivot block should be identical regardless of input form + assert rank == rank_s + assert np.array_equal( + rref_synth.tableau.data[:, :rank], + ref_synth.tableau.data[:, :rank], + ) + class TestBinaryEncodingSynthesizerReplay: """Verify that replaying the circuit on the original matrix produces the final tableau.""" @@ -491,7 +553,7 @@ def test_replay_matches_final_state(self): synth = BinaryEncodingSynthesizer.from_matrix(mat) # Reconstruct by creating a fresh tableau and replaying - replay = RrefTableau(mat.copy()) + replay = RefTableau(mat.copy()) # Permute columns the same way the solver did pivot_cols = [p[1] for p in replay.pivots] From 0219ecaf42a4a54f057dfb6181fbc1c0bf7dd69a Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Tue, 7 Apr 2026 22:15:21 +0000 Subject: [PATCH 08/21] fix doc build --- docs/source/conf.py | 2 +- .../state_preparation/sparse_isometry.py | 2 +- .../sparse_isometry_binary_encoding.py | 14 +++++---- .../qdk_chemistry/utils/binary_encoding.py | 30 +++++++------------ 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9c8f1500a..7f6f3faa7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,13 +59,13 @@ "sphinx.ext.autosummary", # Create summary tables for modules/classes "sphinx.ext.intersphinx", # Link to other projects' documentation "sphinx.ext.viewcode", # Add links to view source code + "sphinx.ext.napoleon", # Support for Google-style and NumPy-style docstrings # Additional extensions "sphinx_autodoc_typehints", # Better support for Python type annotations "sphinx_inline_tabs", # Support for tabbed content in docs # C++ documentation "breathe", # Bridge between Sphinx and Doxygen # Enable Google-style docstrings parsing - "sphinx.ext.napoleon", # Support for Google-style and NumPy-style docstrings "sphinx.ext.todo", # Support for listing to-dos "sphinx.ext.graphviz", # Support for Graphviz diagrams "sphinxcontrib.bibtex", # Support for bibliographic references diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index 2ae39fece..b57b15fb2 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -45,7 +45,7 @@ from qdk_chemistry.utils import Logger from qdk_chemistry.utils.qsharp import QSHARP_UTILS -__all__: list[str] = [] +__all__: list[str] = ["SparseIsometryGF2XStatePreparationSettings"] class SparseIsometryGF2XStatePreparationSettings(StatePreparationSettings): diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index e59f88731..743b80cc8 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -2,9 +2,8 @@ This module implements a state preparation algorithm that combines GF2+X elimination with batched binary encoding. Instead of delegating the reduced -subspace to a dense state preparation routine (as the base -:class:`SparseIsometryGF2XStatePreparation` does), this algorithm feeds the -RREF matrix directly into the binary-encoding solver which synthesises the +subspace to a dense state preparation routine, this algorithm feeds the +REF matrix directly into the binary-encoding solver which synthesises the full circuit using batched Toffoli gates and Partial Unary Iteration (PUI) lookup blocks. """ @@ -32,6 +31,10 @@ gf2x_with_tracking, ) +__all__ = [ + "SparseIsometryBinaryEncodingSettings", +] + class SparseIsometryBinaryEncodingSettings(SparseIsometryGF2XStatePreparationSettings): """Settings for SparseIsometryBinaryEncodingStatePreparation.""" @@ -56,11 +59,10 @@ def __init__(self): class SparseIsometryBinaryEncodingStatePreparation(SparseIsometryGF2XStatePreparation): """State preparation using sparse isometry with binary encoding. - This class extends :class:`SparseIsometryGF2XStatePreparation` by replacing + This class extends sparse isometry with GF2+X elimination by replacing the dense state preparation step with a binary-encoding circuit synthesiser. After GF2+X elimination produces a reduced RREF matrix, the binary-encoding - synthesiser (:class:`~qdk_chemistry.algorithms.state_preparation.binary_encoding.BinaryEncodingSynthesizer`) - compresses the matrix into an efficient circuit using: + synthesiser compresses the matrix into an efficient circuit using: 1. Stage 1 — diagonal (unary-to-binary) encoding of pivot columns 2. Stage 2 — non-pivot column processing with batched PUI lookup blocks diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index caef3f0d6..83c7f11f3 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -1,11 +1,4 @@ -"""Binary encoding circuit synthesiser for RREF matrices. - -This module implements the "binary encoding" step of the GF2+X pipeline: -given a reduced binary matrix (in RREF or upper-staircase diagonal form), -it synthesises a circuit that compresses the sparse rows into a dense -binary-counter register using batched Toffoli gates and Partial Unary -Iteration (PUI) lookup blocks. -""" +"""Binary encoding circuit synthesiser for REF matrices.""" # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. @@ -42,8 +35,7 @@ def _dense_qubits_size(num_cols: int) -> int: num_cols: Number of columns to index. Returns: - Number of qubits needed in the dense register to uniquely index all - columns. + Number of qubits needed in the dense register to uniquely index all columns. """ return 1 if num_cols < 2 else math.ceil(math.log2(num_cols)) @@ -128,8 +120,8 @@ class RefTableau: The input matrix must be in row echelon form (REF), reduced row echelon form (RREF), or upper-staircase diagonal-reduction shape. - The tableau supports in-place updates via the compression operations. + """ def __init__(self, data: np.ndarray): @@ -331,6 +323,7 @@ class BinaryEncodingSynthesizer: a compact binary-counter register via a unary-to-binary ladder. * **Stage 2 — non-pivot processing**: encodes remaining columns using batched Toffoli gates and Partial Unary Iteration (PUI) lookup blocks. + """ def __init__( @@ -1014,8 +1007,8 @@ def _synthesize_single_pui_lookup_block( rest_entries: Per-target offsets and changing controls. Returns: - ``(ops, and_count)`` where ``and_count`` is the - number of emitted ``and`` operations, used as a Toffoli-cost proxy. + ``(ops, and_count)`` where ``and_count`` is the number of + emitted ``and`` operations, used as a Toffoli-cost proxy. """ if not rest_entries: @@ -1054,13 +1047,13 @@ def _canonicalize_pui_controls( Args: fixed_controls: Initial list of fixed controls, as (row, value) pairs. - rest_entries: List of (offset, changing_controls) where changing_controls is a list of - (row, value) pairs that may differ between entries. + rest_entries: List of (offset, changing_controls) where changing_controls is + a list of (row, value) pairs that may differ between entries. Returns: Tuple of (new_fixed_controls, new_rest_entries) - where new_fixed_controls is the updated list of fixed controls and new_rest_entries - is the updated list of entries with promoted controls removed from changing_controls. + where new_fixed_controls is the updated list of fixed controls and new_rest_entries + is the updated list of entries with promoted controls removed from changing_controls. """ if not rest_entries: @@ -1153,8 +1146,7 @@ def _build_pui_lookup_table( address_qubits: List of control rows that will serve as address bits for the lookup. Returns: - Mapping from address bit tuples to one-hot output tuples, with - all-zero outputs omitted. + Mapping from address bit tuples to one-hot output tuples, with all-zero outputs omitted. """ n_outputs = len(rest_entries) From 7b686725a7c5f809a6b633da494133ff62d931e4 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Tue, 7 Apr 2026 22:25:59 +0000 Subject: [PATCH 09/21] fix comments --- examples/state_prep_energy.ipynb | 2 +- .../state_preparation/sparse_isometry.py | 52 +--- .../sparse_isometry_binary_encoding.py | 119 ++------ .../qdk_chemistry/utils/binary_encoding.py | 281 ++++++++++-------- .../utils/qsharp/src/BinaryEncoding.qs | 22 +- python/tests/test_state_preparation.py | 50 +--- .../test_state_preparation_binary_encoding.py | 67 +---- python/tests/test_utils_binary_encoding.py | 198 ++++++------ 8 files changed, 300 insertions(+), 491 deletions(-) diff --git a/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index 83057d76a..82855ee1f 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -362,7 +362,7 @@ "import pandas as pd\n", "from qdk.widgets import Circuit\n", "\n", - "# Generate state preparation circuit for the sparse state via sparse isometry binary_encoding\n", + "# Generate state preparation circuit for the sparse state via sparse isometry with binary encoding\n", "state_prep = create(\"state_prep\", \"sparse_isometry_binary_encoding\")\n", "sparse_isometry_binary_encoding_circuit = state_prep.run(wfn_sparse)\n", "\n", diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index b57b15fb2..6f39f2a15 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -33,8 +33,7 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from dataclasses import dataclass, field -from typing import Any +from dataclasses import dataclass import numpy as np @@ -43,6 +42,7 @@ from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.data.circuit import QsharpFactoryData from qdk_chemistry.utils import Logger +from qdk_chemistry.utils.binary_encoding import MatrixCompressionOp, MatrixCompressionType from qdk_chemistry.utils.qsharp import QSHARP_UTILS __all__: list[str] = ["SparseIsometryGF2XStatePreparationSettings"] @@ -155,9 +155,9 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: if operation[0] == "cx": if isinstance(operation[1], tuple): target, control = operation[1] - expansion_ops.append(MatrixCompressionOp("CX", [control, target])) + expansion_ops.append(MatrixCompressionOp(MatrixCompressionType.CX, [control, target])) elif operation[0] == "x" and isinstance(operation[1], int): - expansion_ops.append(MatrixCompressionOp("X", [operation[1]])) + expansion_ops.append(MatrixCompressionOp(MatrixCompressionType.X, [operation[1]])) # State vector indexing is in little-endian order, the row map is reversed for Q# convention state_prep_params = QSHARP_UTILS.StatePreparation.StatePreparationParams( @@ -450,48 +450,6 @@ class GF2XEliminationResult: """Rank of the reduced matrix (number of non-zero rows).""" -@dataclass -class MatrixCompressionOp: - """A single gate in the compressed matrix-encoding circuit. - - Mirrors the Q# ``MatrixCompressionOp`` struct. Use :meth:`to_dict` to - produce a camelCase dict consumable by the Q# bridge. - - Attributes: - name: Gate name (e.g. ``"CX"``, ``"CCX"``, ``"SELECT"``). - qubits: Qubit indices involved in the operation. - control_state: Integer encoding of the control state for multi- - controlled gates. For ``SELECT``/``SELECT_AND``, this stores - the number of address qubits. - lookup_data: Boolean lookup table for ``SELECT`` operations; - empty list for all other gate types. - - """ - - name: str - qubits: list[int] - control_state: int = 0 - lookup_data: list[list[bool]] = field(default_factory=list) - - def to_dict(self) -> dict[str, Any]: - """Serialize to a camelCase dict matching the Q# ``MatrixCompressionOp`` struct.""" - return { - "name": self.name, - "qubits": self.qubits, - "controlState": self.control_state, - "lookupData": self.lookup_data, - } - - def to_qsharp_parameter(self) -> QSHARP_UTILS.BinaryEncoding.MatrixCompressionOp: - """Convert to a Q# MatrixCompressionOp struct.""" - return QSHARP_UTILS.BinaryEncoding.MatrixCompressionOp( - name=self.name, - qubits=self.qubits, - controlState=self.control_state, - lookupData=self.lookup_data, - ) - - def gf2x_with_tracking( matrix: np.ndarray, *, @@ -842,7 +800,7 @@ def _reduce_diagonal_matrix( operations: Operations list to extend. Returns: - :class:`GF2XEliminationResult` with rank decremented by 1. + GF2XEliminationResult with rank decremented by 1. """ matrix_work = matrix.copy() diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index 743b80cc8..4b1225a84 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -13,19 +13,16 @@ # Licensed under the MIT License. See LICENSE.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from typing import Any - import numpy as np from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.data.circuit import QsharpFactoryData from qdk_chemistry.utils import Logger -from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer +from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer, MatrixCompressionOp, MatrixCompressionType from qdk_chemistry.utils.qsharp import QSHARP_UTILS from .sparse_isometry import ( GF2XEliminationResult, - MatrixCompressionOp, SparseIsometryGF2XStatePreparation, SparseIsometryGF2XStatePreparationSettings, gf2x_with_tracking, @@ -61,7 +58,7 @@ class SparseIsometryBinaryEncodingStatePreparation(SparseIsometryGF2XStatePrepar This class extends sparse isometry with GF2+X elimination by replacing the dense state preparation step with a binary-encoding circuit synthesiser. - After GF2+X elimination produces a reduced RREF matrix, the binary-encoding + After GF2+X elimination produces a REF matrix, the binary-encoding synthesiser compresses the matrix into an efficient circuit using: 1. Stage 1 — diagonal (unary-to-binary) encoding of pivot columns @@ -92,16 +89,6 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: """ Logger.trace_entering() - - # Active Space Consistency Check (same as parent) - alpha_indices, beta_indices = wavefunction.get_orbitals().get_active_space_indices() - if alpha_indices != beta_indices: - raise ValueError( - f"Active space contains {len(alpha_indices)} alpha orbitals and " - f"{len(beta_indices)} beta orbitals. Asymmetric active spaces for " - "alpha and beta orbitals are not supported for state preparation." - ) - coeffs = wavefunction.get_coefficients() dets = wavefunction.get_active_determinants() num_orbitals = len(wavefunction.get_orbitals().get_active_space_indices()[0]) @@ -123,8 +110,8 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: bitstring_matrix = self._bitstrings_to_binary_matrix(bitstrings) gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, forward_only=True) - # Step 2: Binary encoding on the reduced RREF matrix - binary_ops, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) + # Step 2: Binary encoding on the REF matrix + encoded_ops, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) # Step 2b: Build compressed statevector reindexed by the bijection. # The bijection maps (dense_val, orig_col) where orig_col is the @@ -142,34 +129,14 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: dense_row_map = gf2x_result.row_map[:dense_size] # Step 3: Build expansion operations from GF2+X elimination - expansion_ops: list[MatrixCompressionOp] = [] + gaussian_elimination_ops: list[MatrixCompressionOp] = [] for operation in reversed(gf2x_result.operations): if operation[0] in ("cx", "cnot"): if isinstance(operation[1], tuple): target, control = operation[1] - expansion_ops.append(MatrixCompressionOp("CX", [control, target])) + gaussian_elimination_ops.append(MatrixCompressionOp(MatrixCompressionType.CX, [control, target])) elif operation[0] == "x" and isinstance(operation[1], int): - expansion_ops.append(MatrixCompressionOp("X", [operation[1]])) - - # Step 4: Pre-process binary-encoding ops into MatrixCompressionOp instances - encoded_ops = _encode_gf2x_ops_for_qs(binary_ops) - - # Step 4b: Elide redundant CX pair at the boundary. - # Binary encoding's unary staircase always starts with CX(row_map[rank-1], row_map[rank-2]) - # which, after reversal, becomes the LAST encoded_op. - # GF2+X back-substitution often ends with the same CX (clearing the - # entry above the last pivot), which becomes the FIRST expansion_op. - # Since CX is self-inverse, the pair cancels — remove both. - if ( - encoded_ops - and expansion_ops - and encoded_ops[-1].name == "CX" - and expansion_ops[0].name == "CX" - and encoded_ops[-1].qubits == expansion_ops[0].qubits - ): - Logger.debug(f"Eliding redundant boundary CX pair on qubits {encoded_ops[-1].qubits}") - encoded_ops.pop() - expansion_ops.pop(0) + gaussian_elimination_ops.append(MatrixCompressionOp(MatrixCompressionType.X, [operation[1]])) # Build circuit using QDK Q# factory with binary-encoding entry point # dense_val from the bijection uses row 0 = MSB (_bits_to_int is MSB-first). @@ -179,10 +146,9 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: state_prep_params = QSHARP_UTILS.BinaryEncoding.BinaryEncodingStatePreparationParams( rowMap=list(dense_row_map), stateVector=compressed_sv.tolist(), - expansionOps=[op.to_qsharp_parameter() for op in expansion_ops], + gaussianEliminationOps=[op.to_qsharp_parameter() for op in gaussian_elimination_ops], binaryEncodingOps=[op.to_qsharp_parameter() for op in encoded_ops], numQubits=n_qubits, - numAncilla=0, ) qsharp_factory = QsharpFactoryData( @@ -191,7 +157,7 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: ) qsharp_op = QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationOp(*vars(state_prep_params).values()) Logger.info( - f"Binary encoding produced {len(binary_ops)} operations ({len(encoded_ops)} encoded) " + f"Binary encoding produced {len(encoded_ops)} operations " f"for {n_qubits}-qubit system with {len(bitstrings)} determinants" ) @@ -203,17 +169,21 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: def _perform_binary_encoding( self, gf2x_result: GF2XEliminationResult, n_qubits: int - ) -> tuple[list[tuple[str, Any]], list[tuple[int, int]], int]: - """Run binary-encoding synthesis on the reduced RREF matrix. + ) -> tuple[list[MatrixCompressionOp], list[tuple[int, int]], int]: + """Run binary-encoding synthesis and return Q#-ready ops. + + Runs the synthesiser, translates qubit indices from local to global, + and converts operations directly into MatrixCompressionOp + instances in reversed order (so Q# can iterate forward). Args: gf2x_result: Result from GF2+X elimination containing the reduced matrix. n_qubits: Total number of qubits in the original space. Returns: - Tuple of ``(gf2x_ops, bijection, dense_size)``: + Tuple of ``(ops, bijection, dense_size)``: - - ``gf2x_ops``: gate operations from the binary-encoding solver. + - ``ops``: MatrixCompressionOp list ready for Q#. - ``bijection``: list of ``(dense_val, orig_col)`` mapping each original matrix column to its compressed binary-register label. - ``dense_size``: number of qubits in the compressed dense register. @@ -232,68 +202,21 @@ def _perform_binary_encoding( measurement_based_uncompute=self._settings.get("measurement_based_uncompute"), ) - gf2x_ops = synthesizer.to_gf2x_operations( + ops = synthesizer.to_operations( num_local_qubits=n_qubits, active_qubit_indices=gf2x_result.row_map, ancilla_start=n_qubits, + reverse=True, ) Logger.debug( - f"Binary encoding output: {len(gf2x_ops)} ops, " + f"Binary encoding output: {len(ops)} ops, " f"bijection size {len(synthesizer.bijection)}, " f"dense_size {synthesizer.dense_size}" ) - return gf2x_ops, synthesizer.bijection, synthesizer.dense_size + return ops, synthesizer.bijection, synthesizer.dense_size def name(self) -> str: """Return the algorithm identifier string.""" return "sparse_isometry_binary_encoding" - - -def _encode_gf2x_ops_for_qs( - operations: list[tuple[str, Any]], -) -> list[MatrixCompressionOp]: - """Pre-process GF2+X operations into :class:`MatrixCompressionOp` instances for Q#. - - The resulting list is already in *reversed* order so that Q# can iterate - forward. - - Args: - operations: Sequence of ``(op_name, op_args)`` tuples as produced by - :meth:`BinaryEncodingSynthesizer.to_gf2x_operations`. - - Returns: - List of :class:`MatrixCompressionOp` ready to be serialised for Q#. - - """ - ops: list[MatrixCompressionOp] = [] - - for op_name, op_args in reversed(operations): - if op_name == "x": - ops.append(MatrixCompressionOp("X", [int(op_args)])) - - elif op_name == "cx": - ops.append(MatrixCompressionOp("CX", [int(op_args[0]), int(op_args[1])])) - - elif op_name == "swap": - ops.append(MatrixCompressionOp("SWAP", [int(op_args[0]), int(op_args[1])])) - - elif op_name == "ccx": - target, ctrl1, ctrl2 = op_args - ops.append(MatrixCompressionOp("CCX", [int(ctrl1), int(ctrl2), int(target)])) - - elif op_name in ("select", "select_and"): - data_table, addr_qubits, dat_qubits = op_args - qubits = [int(q) for q in addr_qubits] + [int(q) for q in dat_qubits] - qs_name = "SELECT_AND" if op_name == "select_and" else "SELECT" - ops.append( - MatrixCompressionOp( - qs_name, - qubits, - control_state=len(addr_qubits), - lookup_data=data_table, - ) - ) - - return ops diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index 83c7f11f3..d2c2b2280 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -8,13 +8,14 @@ from __future__ import annotations import math -from dataclasses import dataclass -from enum import Enum +from dataclasses import dataclass, field +from enum import StrEnum from typing import TYPE_CHECKING, Any, cast import numpy as np from qdk_chemistry.utils import Logger +from qdk_chemistry.utils.qsharp import QSHARP_UTILS if TYPE_CHECKING: from collections.abc import Iterable @@ -22,6 +23,7 @@ __all__ = [ "BinaryEncodingSynthesizer", + "MatrixCompressionOp", "MatrixCompressionType", "NotRefError", "RefTableau", @@ -72,14 +74,55 @@ class NotRefError(ValueError): """Raised when a matrix is not in row echelon form (REF).""" -class MatrixCompressionType(Enum): - """Operations types recorded during matrix compression.""" +class MatrixCompressionType(StrEnum): + """Supported operation types for matrix compression.""" - CX = "cx" - SWAP = "swap" - TOFFOLI = "toffoli" - PUI_BLOCK = "pui_block" - X = "x" + X = "X" + CX = "CX" + SWAP = "SWAP" + CCX = "CCX" + MCX = "MCX" + SELECT = "SELECT" + SELECT_AND = "SELECT_AND" + + +@dataclass +class MatrixCompressionOp: + """Gate representation for matrix compression operations.""" + + name: MatrixCompressionType + """Gate type, one of the MatrixCompressionType values.""" + qubits: list[int] + """Qubit indices involved in the operation.""" + control_state: int = 0 + """Integer encoding of the control state for multi-controlled gates. + For ``SELECT``/``SELECT_AND``, this stores the number of address qubits.""" + lookup_data: list[list[bool]] = field(default_factory=list) + """Boolean lookup table for ``SELECT`` operations; empty list for all + other gate types.""" + + def __post_init__(self): + """Validate the MatrixCompressionOp parameters.""" + if self.name == MatrixCompressionType.SELECT and not self.lookup_data: + raise ValueError("lookup_data must be provided for SELECT operations") + + def to_dict(self) -> dict[str, Any]: + """Serialize to a camelCase dict matching the Q# ``MatrixCompressionOp`` struct.""" + return { + "name": self.name, + "qubits": self.qubits, + "controlState": self.control_state, + "lookupData": self.lookup_data, + } + + def to_qsharp_parameter(self): + """Convert to a Q# ``MatrixCompressionOp`` struct.""" + return QSHARP_UTILS.BinaryEncoding.MatrixCompressionOp( + name=self.name, + qubits=self.qubits, + controlState=self.control_state, + lookupData=self.lookup_data, + ) def _check_ref(data: np.ndarray) -> None: @@ -87,7 +130,6 @@ def _check_ref(data: np.ndarray) -> None: REF requires non-zero rows to appear before any all-zero rows and each row's leading 1 (pivot) to be strictly to the right of the pivot above. - Unlike RREF, pivot columns may have multiple non-zero entries. Args: data: Binary matrix to validate. @@ -118,8 +160,7 @@ def _check_ref(data: np.ndarray) -> None: class RefTableau: """Binary tableau for the batched sparse-isometry algorithm. - The input matrix must be in row echelon form (REF), reduced row echelon - form (RREF), or upper-staircase diagonal-reduction shape. + The input matrix must be in row echelon form (REF). The tableau supports in-place updates via the compression operations. """ @@ -132,8 +173,7 @@ def __init__(self, data: np.ndarray): basis states. Values are coerced to ``np.int8``. Raises: - NotRefError: If ``data`` is not in REF, RREF, or the accepted - upper-staircase diagonal-reduction form. + NotRefError: If ``data`` is not in REF. AssertionError: If the matrix rank/size assumptions required by the algorithm are violated. @@ -148,7 +188,7 @@ def __init__(self, data: np.ndarray): assert self.dense_size < self.num_rows self._tmp_row = np.zeros(self.num_cols, dtype=np.int8) - self.pivots = self.identify_rref_pivots() + self.pivots = self.identify_pivots() Logger.debug(f"Tableau shape: {self.data.shape}, dense size: {self.dense_size}, pivots: {self.pivots}") @@ -189,7 +229,7 @@ def row_is_zero(self, row: int) -> bool: """ return not np.any(self.data[row]) - def identify_rref_pivots(self) -> list[tuple[int, int]]: + def identify_pivots(self) -> list[tuple[int, int]]: """Find pivot positions using vectorized operations. Returns: @@ -249,7 +289,7 @@ def permute_columns(self, col_order: list[int]): self.data = self.data[:, col_order].copy() self.num_cols = self.data.shape[1] self._tmp_row = np.zeros(self.num_cols, dtype=np.int8) - self.pivots = self.identify_rref_pivots() + self.pivots = self.identify_pivots() def toffoli(self, target: int, ctrl0: tuple[int, bool], ctrl1: tuple[int, bool]): """Apply a two-control conditional XOR into ``target``. @@ -268,40 +308,25 @@ def toffoli(self, target: int, ctrl0: tuple[int, bool], ctrl1: tuple[int, bool]) m1 = self.data[c1] if v1 else (1 - self.data[c1]) self.data[target] ^= m0 & m1 - def _and_controls_into_mask(self, mask: np.ndarray, controls: list[tuple[int, bool]]) -> None: - """Constrain a mask by conjunction over signed controls. - - Args: - mask: Mutable bit-mask updated in place. - controls: Control predicates ``(row, required_value)``. - - """ - for row, val in controls: - mask &= self.data[row] if val else (1 - self.data[row]) - - def toffoli_pui_fixed(self, controls: list[tuple[int, bool]]): - """Initialize shared PUI mask from fixed controls. - - Args: - controls: Controls common to every target in one PUI block. - - """ - self._tmp_row[:] = 1 - self._and_controls_into_mask(self._tmp_row, controls) + def select(self, data_table: list[list[bool]], addr_qubits: list[int], dat_qubits: list[int]): + """Apply a SELECT lookup operation to the tableau. - def toffoli_pui_rest(self, target_row_offset: int, controls: list[tuple[int, bool]]): - """Apply one branch of a prepared PUI update. + For each column, compute an address from ``addr_qubits`` rows + (little-endian), look up the corresponding ``data_table`` entry, + and XOR the data bits into the ``dat_qubits`` rows. Args: - target_row_offset: Offset from dense/sparse boundary to the target - sparse row. - controls: Additional branch-specific controls. + data_table: Dense Boolean lookup table indexed by address integer. + addr_qubits: Row indices used as address bits (index 0 = LSB). + dat_qubits: Row indices to XOR data into. """ - target = self.dense_size + target_row_offset - mask = self._tmp_row.copy() - self._and_controls_into_mask(mask, controls) - self.data[target] ^= mask + addr_vals = np.zeros(self.num_cols, dtype=int) + for i, q in enumerate(addr_qubits): + addr_vals += self.data[q].astype(int) << i + for j, dq in enumerate(dat_qubits): + mask = np.array([data_table[a][j] for a in addr_vals], dtype=np.int8) + self.data[dq] ^= mask @dataclass @@ -315,7 +340,7 @@ class _BatchElement: class BinaryEncodingSynthesizer: - """Synthesise a circuit from a binary RREF tableau using batched sparse isometry. + """Synthesise a circuit from a binary REF tableau using batched sparse isometry. The synthesiser executes a two-stage algorithm: @@ -376,8 +401,7 @@ def from_matrix( synthesiser. Args: - matrix: Binary (0/1) matrix in REF, RREF, or upper-staircase - diagonal form, shaped ``(num_qubits, num_determinants)``. + matrix: Binary (0/1) matrix in REF form, shaped ``(num_qubits, num_determinants)``. include_negative_controls: If True, include both positive and negative (0-valued) fixed controls in PUI blocks. If False, only positive (1-valued) controls are emitted. @@ -420,26 +444,23 @@ def _record(self, op: tuple[MatrixCompressionType, tuple[Any, ...]]): """Append an operation and update the tableau. Args: - op: Operation to record, as a tuple of (MatrixCompressionType, payload). + op: Operation to record, as a tuple of (MatrixCompressionType, qubit_args). """ self.circuit.append(op) - compress_type, payload = op + compress_type, qubit_args = op if compress_type is MatrixCompressionType.CX: - self.tableau.cx(*payload) + self.tableau.cx(*qubit_args) elif compress_type is MatrixCompressionType.SWAP: - self.tableau.swap(*payload) - elif compress_type is MatrixCompressionType.TOFFOLI: - tgt, ctrl_pos, ctrl_row, ctrl_val = payload - self.tableau.toffoli(tgt, (ctrl_pos, True), (ctrl_row, ctrl_val)) - elif compress_type is MatrixCompressionType.PUI_BLOCK: - fixed_controls, rest_entries = payload - self.tableau.toffoli_pui_fixed(fixed_controls) - for off, ctrls in rest_entries: - self.tableau.toffoli_pui_rest(off, ctrls) + self.tableau.swap(*qubit_args) + elif compress_type is MatrixCompressionType.CCX: + self.tableau.toffoli(qubit_args[0], (qubit_args[1], True), (qubit_args[2], True)) + elif compress_type in {MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND}: + data_table, addr_qubits, dat_qubits = qubit_args + self.tableau.select(data_table, addr_qubits, dat_qubits) elif compress_type is MatrixCompressionType.X: - self.tableau.x(*payload) + self.tableau.x(*qubit_args) def run(self): """Execute full synthesis and restore original column order.""" @@ -521,8 +542,7 @@ def _apply_unary_staircase(self, rank: int) -> list[int]: Inspects each above-diagonal entry in the pivot block (columns 0 … rank-1 after pivot permutation) and emits a CX to fill any - missing 1. This handles RREF (identity), REF (upper-triangular), - and staircase (already filled) inputs uniformly. + missing 1 Processing columns left-to-right ensures side effects on later columns are absorbed when they are reached. @@ -571,7 +591,7 @@ def _convert_unary_to_binary(self, limit: int, logical_rows: list[int]): x, y = unary_bits[2 * p], unary_bits[2 * p + 1] self._record((MatrixCompressionType.CX, (x, accumulator))) self._record((MatrixCompressionType.CX, (y, accumulator))) - self._record((MatrixCompressionType.TOFFOLI, (y, accumulator, x, True))) + self._record((MatrixCompressionType.CCX, (y, accumulator, x))) next_active_unary.append(x) zero_rows.append(y) @@ -761,7 +781,11 @@ def _synthesize_target_row(self, target_row: int, col: int, row: int): diff_row = int(np.flatnonzero(diffs)[0]) diff_val = bool(col_data[diff_row]) - self._record((MatrixCompressionType.TOFFOLI, (target_row, row, diff_row, diff_val))) + if not diff_val: + self._record((MatrixCompressionType.X, (diff_row,))) + self._record((MatrixCompressionType.CCX, (target_row, row, diff_row))) + if not diff_val: + self._record((MatrixCompressionType.X, (diff_row,))) def _permute_col_and_add_to_batch(self, current_col: int, ctrl_row: int): """Normalize a column's dense/sparse bits and append it to the batch. @@ -831,15 +855,20 @@ def _clear_sparse_bits(self): ] rest_entries.append((i, changing_controls)) - self._record((MatrixCompressionType.PUI_BLOCK, (fixed_controls, rest_entries))) + select_ops: list[tuple[MatrixCompressionType, Any]] = [] + self._flush_pui_lookup_block(select_ops, dense_size, fixed_controls, rest_entries) + for op in select_ops: + self._record(op) - def to_gf2x_operations( + def to_operations( self, num_local_qubits: int, active_qubit_indices: list[int] | None = None, ancilla_start: int | None = None, - ) -> list[tuple[str, Any]]: - """Translate recorded circuit operations into GF2+X instruction tuples. + *, + reverse: bool = False, + ) -> list[MatrixCompressionOp]: + """Translate recorded circuit operations into MatrixCompressionOp instances. Args: num_local_qubits: Number of local (active) qubits. @@ -847,48 +876,64 @@ def to_gf2x_operations( to global qubit index. If provided, operations are translated to global indices. ancilla_start: Optional global starting index for ancillas. Used if active_qubit_indices is provided. + reverse: If True, reverse the operation order before returning. Returns: - List of generated operations (optionally translated to global indices). + List of MatrixCompressionOp. """ - ops: list[tuple[str, Any]] = [] - - for compress_type, payload in self.circuit: - if compress_type is MatrixCompressionType.CX: - ops.append(("cx", payload)) - elif compress_type is MatrixCompressionType.SWAP: - ops.append(("swap", payload)) - elif compress_type is MatrixCompressionType.TOFFOLI: - target, ctrl_pos, ctrl_row, ctrl_val = payload - if not ctrl_val: - ops.append(("x", ctrl_row)) - ops.append(("ccx", (target, ctrl_pos, ctrl_row))) - if not ctrl_val: - ops.append(("x", ctrl_row)) - elif compress_type is MatrixCompressionType.X: - ops.append(("x", payload[0])) - elif compress_type is MatrixCompressionType.PUI_BLOCK: - fixed_controls, rest_entries = payload - self._flush_pui_lookup_block( - ops, - self.dense_size, - fixed_controls, - rest_entries, - ) + raw_ops: list[tuple[MatrixCompressionType, Any]] = [] + + for compress_type, qubit_args in self.circuit: + if compress_type is MatrixCompressionType.X: + raw_ops.append((compress_type, qubit_args[0])) + else: + raw_ops.append((compress_type, qubit_args)) if active_qubit_indices is not None and ancilla_start is not None: - ops = self._translate_ops(ops, num_local_qubits, active_qubit_indices, ancilla_start) + raw_ops = self._translate_ops(raw_ops, num_local_qubits, active_qubit_indices, ancilla_start) + ops = [self._to_compression_op(op_type, op_args) for op_type, op_args in raw_ops] + if reverse: + ops.reverse() return ops + @staticmethod + def _to_compression_op(op_type: MatrixCompressionType, op_args: Any) -> MatrixCompressionOp: + """Convert a raw circuit tuple into a MatrixCompressionOp. + + Args: + op_type: The gate type. + op_args: Gate arguments (qubit indices and optional data). + + Returns: + A MatrixCompressionOp instance. + + """ + if op_type is MatrixCompressionType.X: + return MatrixCompressionOp(op_type, [int(op_args)]) + if op_type in {MatrixCompressionType.CX, MatrixCompressionType.SWAP}: + return MatrixCompressionOp(op_type, [int(op_args[0]), int(op_args[1])]) + if op_type is MatrixCompressionType.CCX: + target, ctrl1, ctrl2 = op_args + return MatrixCompressionOp(op_type, [int(ctrl1), int(ctrl2), int(target)]) + if op_type in {MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND}: + data_table, addr_qubits, dat_qubits = op_args + qubits = [int(q) for q in addr_qubits] + [int(q) for q in dat_qubits] + return MatrixCompressionOp(op_type, qubits, control_state=len(addr_qubits), lookup_data=data_table) + if op_type is MatrixCompressionType.MCX: + controls, _, target_qubit = op_args + qubits = [int(q) for q in controls] + [int(target_qubit)] + return MatrixCompressionOp(op_type, qubits, control_state=len(controls)) + raise ValueError(f"Unknown op type: {op_type}") + @staticmethod def _translate_ops( - ops: list[tuple[str, Any]], + ops: list[tuple[MatrixCompressionType, Any]], num_local_qubits: int, active_qubit_indices: list[int], ancilla_start: int, - ) -> list[tuple[str, Any]]: + ) -> list[tuple[MatrixCompressionType, Any]]: """Remap local qubit indices to global topological indices. Indices below ``num_local_qubits`` are mapped through @@ -911,19 +956,19 @@ def map_idx(idx: int) -> int: int(active_qubit_indices[idx]) if idx < num_local_qubits else ancilla_start + (idx - num_local_qubits) ) - translated: list[tuple[str, Any]] = [] - for op_name, op_args in ops: - if op_name == "x": - translated.append(("x", map_idx(int(op_args)))) - elif op_name in {"cx", "swap"}: - translated.append((op_name, (map_idx(int(op_args[0])), map_idx(int(op_args[1]))))) - elif op_name in {"ccx"}: - translated.append((op_name, tuple(map_idx(int(a)) for a in op_args))) - elif op_name == "mcx": + translated: list[tuple[MatrixCompressionType, Any]] = [] + for op_type, op_args in ops: + if op_type is MatrixCompressionType.X: + translated.append((MatrixCompressionType.X, map_idx(int(op_args)))) + elif op_type in {MatrixCompressionType.CX, MatrixCompressionType.SWAP}: + translated.append((op_type, (map_idx(int(op_args[0])), map_idx(int(op_args[1]))))) + elif op_type is MatrixCompressionType.CCX: + translated.append((op_type, tuple(map_idx(int(a)) for a in op_args))) + elif op_type is MatrixCompressionType.MCX: controls, ctrl_state, target = op_args translated.append( ( - "mcx", + MatrixCompressionType.MCX, ( [map_idx(int(q)) for q in controls], list(ctrl_state), @@ -931,11 +976,11 @@ def map_idx(idx: int) -> int: ), ) ) - elif op_name in ("select", "select_and"): + elif op_type in {MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND}: data_table, addr_qubits, dat_qubits = op_args translated.append( ( - op_name, + op_type, ( data_table, [map_idx(int(q)) for q in addr_qubits], @@ -944,12 +989,12 @@ def map_idx(idx: int) -> int: ) ) else: - translated.append((op_name, op_args)) + translated.append((op_type, op_args)) return translated def _flush_pui_lookup_block( self, - ops: list[tuple[str, Any]], + ops: list[tuple[MatrixCompressionType, Any]], sbs: int, fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], @@ -998,7 +1043,7 @@ def _synthesize_single_pui_lookup_block( sbs: int, fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], - ) -> tuple[list[tuple[str, Any]], int]: + ) -> tuple[list[tuple[MatrixCompressionType, Any]], int]: """Lower one PUI sub-block into lookup ops. Args: @@ -1029,7 +1074,7 @@ def _synthesize_single_pui_lookup_block( use_measurement_and=self.measurement_based_uncompute, ) - typed_lookup_ops = cast("list[tuple[str, Any]]", lookup_ops) + typed_lookup_ops = cast("list[tuple[MatrixCompressionType, Any]]", lookup_ops) gf2x_ops = list(reversed(typed_lookup_ops)) and_count = sum(1 for name, _ in typed_lookup_ops if name == "and") @@ -1166,7 +1211,7 @@ def _lookup_select( data_qubits: list[int], *, use_measurement_and: bool = False, -) -> list[tuple[str, Any]]: +) -> list[tuple[MatrixCompressionType, Any]]: """Synthesize a lookup-based select or select_and operation for a given truth table. Args: @@ -1185,7 +1230,7 @@ def _lookup_select( if not table_dict: return [] - operations: list[tuple[str, Any]] = [] + operations: list[tuple[MatrixCompressionType, Any]] = [] n_address = len(address_qubits) n_data = len(data_qubits) @@ -1200,7 +1245,7 @@ def _lookup_select( # Our table index also uses little-endian (addr_tuple[0] = LSB), so # pass address_qubits directly without reversing. - op_name = "select_and" if use_measurement_and else "select" - operations.append((op_name, (data_table, list(address_qubits), list(data_qubits)))) + op_type = MatrixCompressionType.SELECT_AND if use_measurement_and else MatrixCompressionType.SELECT + operations.append((op_type, (data_table, list(address_qubits), list(data_qubits)))) return operations diff --git a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs index 806134bba..6cd1a7485 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs @@ -37,13 +37,11 @@ namespace QDKChemistry.Utils.BinaryEncoding { /// Amplitudes of the reduced-space statevector. stateVector : Double[], /// GF2+X expansion operations (CX / X) to reverse the GF2+X elimination. - expansionOps : MatrixCompressionOp[], + gaussianEliminationOps : MatrixCompressionOp[], /// Binary-encoding gate sequence (already reversed by Python). binaryEncodingOps : MatrixCompressionOp[], - /// Total number of qubits (system + ancilla). + /// Total number of qubits. numQubits : Int, - /// Number of ancilla qubits required by the binary-encoding circuit. - numAncilla : Int, } /// Apply a single matrix-compression gate to a qubit register. @@ -260,7 +258,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { } // Step 3: Expand back via GF2+X operations - for gate in params.expansionOps { + for gate in params.gaussianEliminationOps { ApplyMatrixCompressionOp(gate, qs); } } @@ -269,18 +267,16 @@ namespace QDKChemistry.Utils.BinaryEncoding { function MakeBinaryEncodingStatePreparationOp( rowMap : Int[], stateVector : Double[], - expansionOps : MatrixCompressionOp[], + gaussianEliminationOps : MatrixCompressionOp[], binaryEncodingOps : MatrixCompressionOp[], numQubits : Int, - numAncilla : Int, ) : Qubit[] => Unit { BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { rowMap = rowMap, stateVector = stateVector, - expansionOps = expansionOps, + gaussianEliminationOps = gaussianEliminationOps, binaryEncodingOps = binaryEncodingOps, numQubits = numQubits, - numAncilla = numAncilla, }, _) } @@ -288,19 +284,17 @@ namespace QDKChemistry.Utils.BinaryEncoding { operation MakeBinaryEncodingStatePreparationCircuit( rowMap : Int[], stateVector : Double[], - expansionOps : MatrixCompressionOp[], + gaussianEliminationOps : MatrixCompressionOp[], binaryEncodingOps : MatrixCompressionOp[], numQubits : Int, - numAncilla : Int, ) : Unit { - use qs = Qubit[numQubits + numAncilla]; + use qs = Qubit[numQubits]; BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { rowMap = rowMap, stateVector = stateVector, - expansionOps = expansionOps, + gaussianEliminationOps = gaussianEliminationOps, binaryEncodingOps = binaryEncodingOps, numQubits = numQubits, - numAncilla = numAncilla, }, qs); } } diff --git a/python/tests/test_state_preparation.py b/python/tests/test_state_preparation.py index a73517701..b06d5cc6c 100644 --- a/python/tests/test_state_preparation.py +++ b/python/tests/test_state_preparation.py @@ -20,7 +20,7 @@ import pytest import qsharp -from qdk_chemistry.algorithms import available, create +from qdk_chemistry.algorithms import create from qdk_chemistry.algorithms.state_preparation.sparse_isometry import ( GF2XEliminationResult, SparseIsometryGF2XStatePreparation, @@ -310,52 +310,6 @@ def test_prepare_single_reference_state_error_cases(): test_cls._prepare_single_reference_state("1012") -def test_asymmetric_active_space_error(): - """Test error for asymmetric active space in StatePrep.""" - - class MockOrbitals: - """Mock orbitals with asymmetric active space indices.""" - - def get_active_space_indices(self): - """Return asymmetric active space indices.""" - return ([0, 1, 2], [0, 1, 2, 3]) - - class MockWavefunction: - """Mock wavefunction for testing asymmetric active space.""" - - def get_orbitals(self): - """Return mock orbitals.""" - return MockOrbitals() - - def get_active_determinants(self): - """Return mock determinants.""" - return [Configuration("2020000"), Configuration("2200000")] - - def get_coefficient(self, _): - """Return mock coefficient.""" - return 1.0 - - def get_coefficients(self): - """Return coefficients for all determinants.""" - return [1.0, 0.5] # Two coefficients for the two determinants - - def size(self): - """Return the number of determinants.""" - return len(self.get_active_determinants()) - - mock_wfn = MockWavefunction() - for sp_key in available("state_prep"): - prep = create("state_prep", sp_key) - with pytest.raises( - ValueError, - match=re.escape( - "Active space contains 3 alpha orbitals and 4 beta orbitals. Asymmetric active spaces for " - "alpha and beta orbitals are not supported for state preparation." - ), - ): - prep.run(mock_wfn) - - def test_find_pivot_row(): """Test the _find_pivot_row helper function.""" # Test matrix with clear pivot patterns @@ -969,7 +923,7 @@ def test_forward_only_produces_upper_triangular(): def test_forward_only_does_not_back_substitute(): - """Forward-only differs from full RREF — above-diagonal entries survive.""" + """Forward-only differs from full RREF, above-diagonal entries survive.""" matrix = np.array([[1, 1, 0], [0, 1, 1], [1, 0, 1]], dtype=np.int8) m_fwd, _, _ = _perform_gaussian_elimination(matrix, [0, 1, 2], [], forward_only=True) m_full, _, _ = _perform_gaussian_elimination(matrix, [0, 1, 2], []) diff --git a/python/tests/test_state_preparation_binary_encoding.py b/python/tests/test_state_preparation_binary_encoding.py index 2f1207e65..a2aed23d0 100644 --- a/python/tests/test_state_preparation_binary_encoding.py +++ b/python/tests/test_state_preparation_binary_encoding.py @@ -12,10 +12,9 @@ from qdk_chemistry.algorithms import create from qdk_chemistry.algorithms.state_preparation import SparseIsometryBinaryEncodingStatePreparation from qdk_chemistry.algorithms.state_preparation.sparse_isometry import gf2x_with_tracking -from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import _encode_gf2x_ops_for_qs from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.plugins.qiskit import QDK_CHEMISTRY_HAS_QISKIT -from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer +from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer, MatrixCompressionType from .reference_tolerances import float_comparison_absolute_tolerance, float_comparison_relative_tolerance from .test_helpers import create_random_wavefunction @@ -43,16 +42,15 @@ def _matrix_qubit_counts(wf: Wavefunction) -> tuple[int, int]: gf2x_result = gf2x_with_tracking(matrix, skip_diagonal_reduction=True, forward_only=True) synthesizer = BinaryEncodingSynthesizer.from_matrix(gf2x_result.reduced_matrix) - ops = synthesizer.to_gf2x_operations( + ops = synthesizer.to_operations( num_local_qubits=n_system, active_qubit_indices=gf2x_result.row_map, ancilla_start=n_system, ) max_select_ancilla = 0 - for op_name, op_args in ops: - if op_name in ("select", "select_and"): - _, address_qubits, _ = op_args - n_addr = len(address_qubits) + for op in ops: + if op.name in (MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND): + n_addr = op.control_state max_select_ancilla = max(max_select_ancilla, n_addr - 1) return n_system, max_select_ancilla @@ -199,58 +197,3 @@ def test_random_wavefunction_statevector(self, n_electrons, n_orbitals, n_dets, assert np.isclose( overlap, 1.0, atol=float_comparison_absolute_tolerance, rtol=float_comparison_relative_tolerance ) - - def test_encode_empty_ops(self): - """Empty ops list produces empty encoded list.""" - assert _encode_gf2x_ops_for_qs([]) == [] - - def test_encode_cx(self): - """CX op is encoded as MatrixCompressionOp('CX', ...).""" - encoded = _encode_gf2x_ops_for_qs([("cx", (0, 1))]) - assert len(encoded) == 1 - assert encoded[0].name == "CX" - assert encoded[0].qubits == [0, 1] - - def test_encode_x(self): - """X op is encoded as MatrixCompressionOp('X', [qubit]).""" - encoded = _encode_gf2x_ops_for_qs([("x", 5)]) - assert encoded[0].name == "X" - assert encoded[0].qubits == [5] - - def test_encode_swap(self): - """SWAP op is encoded correctly.""" - encoded = _encode_gf2x_ops_for_qs([("swap", (2, 3))]) - assert encoded[0].name == "SWAP" - assert encoded[0].qubits == [2, 3] - - def test_encode_ccx(self): - """CCX op encodes [ctrl1, ctrl2, target].""" - encoded = _encode_gf2x_ops_for_qs([("ccx", (4, 0, 1))]) - assert encoded[0].name == "CCX" - assert encoded[0].qubits == [0, 1, 4] - - def test_encode_select(self): - """SELECT op preserves data table and records address qubit count.""" - data = [[True, False], [False, True]] - encoded = _encode_gf2x_ops_for_qs([("select", (data, [0, 1], [2, 3]))]) - assert encoded[0].name == "SELECT" - assert encoded[0].qubits == [0, 1, 2, 3] - assert encoded[0].control_state == 2 - assert encoded[0].lookup_data is data - - def test_encode_select_and(self): - """SELECT_AND is used for measurement-based ops.""" - encoded = _encode_gf2x_ops_for_qs([("select_and", ([[True]], [0], [1]))]) - assert encoded[0].name == "SELECT_AND" - - def test_encode_reversed_order(self): - """Encoded ops appear in reversed order relative to input.""" - encoded = _encode_gf2x_ops_for_qs([("x", 0), ("cx", (1, 2)), ("x", 3)]) - assert [op.name for op in encoded] == ["X", "CX", "X"] - assert encoded[0].qubits == [3] - assert encoded[2].qubits == [0] - - def test_encode_to_dict_keys(self): - """to_dict produces camelCase keys required by Q# bridge.""" - encoded = _encode_gf2x_ops_for_qs([("cx", (0, 1))]) - assert set(encoded[0].to_dict().keys()) == {"name", "qubits", "controlState", "lookupData"} diff --git a/python/tests/test_utils_binary_encoding.py b/python/tests/test_utils_binary_encoding.py index 2794de5f5..d85ab9886 100644 --- a/python/tests/test_utils_binary_encoding.py +++ b/python/tests/test_utils_binary_encoding.py @@ -132,7 +132,7 @@ class TestRefTableau: """Tests for RefTableau construction and gate operations.""" def _make_ref(self, n_pivots: int, n_extra_cols: int) -> RefTableau: - """Build a realistic RREF tableau with fill in non-pivot columns. + """Build a realistic REF tableau with fill in non-pivot columns. The pivot block is an identity matrix. Non-pivot columns get alternating 0/1 entries (a common pattern after Gaussian @@ -156,8 +156,8 @@ def _make_ref(self, n_pivots: int, n_extra_cols: int) -> RefTableau: mat[r, c] = (r + c) % 2 return RefTableau(mat) - def test_construction_from_rref(self): - """Valid RREF matrix produces a tableau with correct dimensions and pivots.""" + def test_construction_from_ref(self): + """Valid REF matrix produces a tableau with correct dimensions and pivots.""" t = self._make_ref(3, 2) assert t.num_rows == 4 assert t.num_cols == 5 @@ -219,7 +219,7 @@ def test_toffoli_negative_control(self): t.toffoli(2, (0, True), (1, False)) np.testing.assert_array_equal(t.data[2], [1, 0, 0, 1]) - def test_identify_rref_pivots(self): + def test_identify_pivots(self): """Pivot detection returns (row, col) pairs for each leading 1.""" mat = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 0, 0]], dtype=np.int8) t = RefTableau(mat) @@ -232,35 +232,12 @@ def test_permute_columns(self): np.testing.assert_array_equal(t.data[0], [0, 0, 1]) np.testing.assert_array_equal(t.data[2], [1, 0, 0]) - def test_toffoli_pui_fixed_and_rest(self): - """Test PUI initialization and per-branch application.""" - mat = np.array( - [ - [1, 0, 1, 0], - [0, 1, 0, 1], - [0, 0, 0, 0], - [0, 0, 0, 0], - ], - dtype=np.int8, - ) - t = RefTableau(mat) - # Fixed control: row 0 must be 1 - t.toffoli_pui_fixed([(0, True)]) - # _tmp_row should be row0 = [1, 0, 1, 0] - np.testing.assert_array_equal(t._tmp_row, [1, 0, 1, 0]) - - # Apply rest: target offset 0 (row dense_size + 0), control: row 1 True - # mask = _tmp_row & row1 = [1,0,1,0] & [0,1,0,1] = [0,0,0,0] - t.toffoli_pui_rest(0, [(1, True)]) - # Row dense_size+0 should be unchanged (XOR with zeros) - np.testing.assert_array_equal(t.data[t.dense_size], [0, 0, 0, 0]) - class TestBinaryEncodingSynthesizerBasic: """Basic construction and property tests.""" def test_from_matrix_identity(self): - """Identity RREF matrix should produce a valid synthesiser.""" + """Identity REF matrix should produce a valid synthesiser.""" mat = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]], dtype=np.int8) synth = BinaryEncodingSynthesizer.from_matrix(mat) assert synth.dense_size == _dense_qubits_size(4) @@ -312,7 +289,7 @@ def test_include_negative_controls_preserves_bijection_semantics(self): class TestBinaryEncodingSynthesizerBijection: """End-to-end compression correctness for BinaryEncodingSynthesizer. - Each parametrized RREF matrix is fed through from_matrix(); the tests + Each parametrized REF matrix is fed through from_matrix(); the tests verify that the bijection faithfully represents the compressed output. """ @@ -320,16 +297,17 @@ class TestBinaryEncodingSynthesizerBijection: params=[ "identity_3x4", "identity_4x5", - "rref_with_fill", - "wide_rref", + "ref_with_fill", + "wide_ref", "minimal_3x3", "staircase_4x5", "all_pivot_4x5", "many_non_pivot_5x8", + "upper_triangular_5x8", ] ) - def rref_matrix(self, request) -> np.ndarray: - """Parametrized RREF matrices covering various shapes.""" + def ref_matrix(self, request) -> np.ndarray: + """Parametrized REF matrices covering various shapes.""" matrices = { # 3 pivots, 1 non-pivot, 1 trailing zero row "identity_3x4": np.array( @@ -342,7 +320,7 @@ def rref_matrix(self, request) -> np.ndarray: dtype=np.int8, ), # 4 pivots, 2 non-pivot columns with fill - "rref_with_fill": np.array( + "ref_with_fill": np.array( [ [1, 0, 0, 0, 1, 1], [0, 1, 0, 0, 0, 1], @@ -352,7 +330,7 @@ def rref_matrix(self, request) -> np.ndarray: dtype=np.int8, ), # 5 pivots, 3 non-pivot columns (wide matrix, dense_size=3) - "wide_rref": np.array( + "wide_ref": np.array( [ [1, 0, 0, 0, 0, 1, 1, 0], [0, 1, 0, 0, 0, 0, 1, 1], @@ -393,43 +371,54 @@ def rref_matrix(self, request) -> np.ndarray: ], dtype=np.int8, ), + # Larger upper-triangular REF with non-pivot columns + "upper_triangular_5x8": np.array( + [ + [1, 1, 1, 1, 1, 1, 0, 1], + [0, 1, 1, 1, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.int8, + ), } return matrices[request.param] - def test_bijection_covers_all_columns(self, rref_matrix): + def test_bijection_covers_all_columns(self, ref_matrix): """Every column must appear exactly once in the bijection.""" - synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + synth = BinaryEncodingSynthesizer.from_matrix(ref_matrix) cols = [c for _, c in synth.bijection] - assert sorted(cols) == list(range(rref_matrix.shape[1])) + assert sorted(cols) == list(range(ref_matrix.shape[1])) - def test_bijection_dense_labels_unique(self, rref_matrix): + def test_bijection_dense_labels_unique(self, ref_matrix): """Dense labels must be unique.""" - synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + synth = BinaryEncodingSynthesizer.from_matrix(ref_matrix) dense_vals = [dv for dv, _ in synth.bijection] assert len(set(dense_vals)) == len(dense_vals) - def test_bijection_dense_labels_fit_in_register(self, rref_matrix): + def test_bijection_dense_labels_fit_in_register(self, ref_matrix): """All dense labels must fit in the dense register.""" - synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + synth = BinaryEncodingSynthesizer.from_matrix(ref_matrix) max_label = (1 << synth.dense_size) - 1 for dv, _ in synth.bijection: assert 0 <= dv <= max_label - def test_sparse_rows_zeroed_after_synthesis(self, rref_matrix): + def test_sparse_rows_zeroed_after_synthesis(self, ref_matrix): """After synthesis, all sparse rows should be all-zero.""" - synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + synth = BinaryEncodingSynthesizer.from_matrix(ref_matrix) for row in range(synth.dense_size, synth.tableau.num_rows): assert synth.tableau.row_is_zero(row) - def test_dense_register_matches_bijection(self, rref_matrix): + def test_dense_register_matches_bijection(self, ref_matrix): """Reading dense rows of each column must reproduce the bijection label. This is the core compression-correctness check: the synthesizer - transforms the original RREF matrix so that the top ``dense_size`` + transforms the original REF matrix so that the top ``dense_size`` rows encode a binary label for every column, and that label matches what the bijection records. """ - synth = BinaryEncodingSynthesizer.from_matrix(rref_matrix) + synth = BinaryEncodingSynthesizer.from_matrix(ref_matrix) ds = synth.dense_size for dense_val, col in synth.bijection: actual = _bits_to_int(synth.tableau.data[:ds, col]) @@ -473,26 +462,24 @@ def test_stage1_starts_with_cx_or_x(self): ids=["6e6o_5det", "8e10o_20det", "10e15o_30det", "14e20o_50det"], ) def test_gf2x_forward_only_fewer_cx_than_rref(self, n_electrons, n_orbitals, n_dets, seed): - """Test forward_only produces fewer CX than RREF.""" + """Test forward_only (REF) produces fewer CX than back-substituted (RREF).""" raw_matrix = create_random_bitstring_matrix( n_electrons=n_electrons, n_orbitals=n_orbitals, n_dets=n_dets, seed=seed ) - # --- RREF path --- + # --- RREF path (back-substituted) --- rref_result = gf2x_with_tracking(raw_matrix, skip_diagonal_reduction=True) - rref_tableau = RefTableau(rref_result.reduced_matrix) - rref_synth = BinaryEncodingSynthesizer(rref_tableau) + rref_synth = BinaryEncodingSynthesizer(RefTableau(rref_result.reduced_matrix)) rank, _ = rref_synth._permute_columns_pivots_first() rref_synth._apply_unary_staircase(rank) rref_cx = sum(1 for t, _ in rref_synth.circuit if t is MatrixCompressionType.CX) assert rref_cx == rank * (rank - 1) // 2 - # --- REF path --- + # --- REF path (forward-only) --- ref_result = gf2x_with_tracking(raw_matrix, forward_only=True) - ref_tableau = RefTableau(ref_result.reduced_matrix) - ref_synth = BinaryEncodingSynthesizer(ref_tableau) - rank_s, _ = ref_synth._permute_columns_pivots_first() - ref_synth._apply_unary_staircase(rank_s) + ref_synth = BinaryEncodingSynthesizer(RefTableau(ref_result.reduced_matrix)) + rank_ref, _ = ref_synth._permute_columns_pivots_first() + ref_synth._apply_unary_staircase(rank_ref) ref_cx = sum(1 for t, _ in ref_synth.circuit if t is MatrixCompressionType.CX) assert ref_cx < rref_cx @@ -512,21 +499,19 @@ def test_stage1_forward_only_fewer_cx_than_rref(self, n_electrons, n_orbitals, n n_electrons=n_electrons, n_orbitals=n_orbitals, n_dets=n_dets, seed=seed ) - # --- RREF path --- + # --- RREF path (back-substituted) --- rref_result = gf2x_with_tracking(raw_matrix, skip_diagonal_reduction=True) - rref_tableau = RefTableau(rref_result.reduced_matrix) - rref_synth = BinaryEncodingSynthesizer(rref_tableau) + rref_synth = BinaryEncodingSynthesizer(RefTableau(rref_result.reduced_matrix)) rank, _ = rref_synth._permute_columns_pivots_first() rref_synth._run_stage1_diagonal_encoding(rank) rref_cx = sum(1 for t, _ in rref_synth.circuit if t is MatrixCompressionType.CX) rref_x = sum(1 for t, _ in rref_synth.circuit if t is MatrixCompressionType.X) - # --- REF path --- + # --- REF path (forward-only) --- ref_result = gf2x_with_tracking(raw_matrix, forward_only=True) - ref_tableau = RefTableau(ref_result.reduced_matrix) - ref_synth = BinaryEncodingSynthesizer(ref_tableau) - rank_s, _ = ref_synth._permute_columns_pivots_first() - ref_synth._run_stage1_diagonal_encoding(rank_s) + ref_synth = BinaryEncodingSynthesizer(RefTableau(ref_result.reduced_matrix)) + rank_ref, _ = ref_synth._permute_columns_pivots_first() + ref_synth._run_stage1_diagonal_encoding(rank_ref) ref_cx = sum(1 for t, _ in ref_synth.circuit if t is MatrixCompressionType.CX) ref_x = sum(1 for t, _ in ref_synth.circuit if t is MatrixCompressionType.X) @@ -534,7 +519,7 @@ def test_stage1_forward_only_fewer_cx_than_rref(self, n_electrons, n_orbitals, n assert ref_x == rref_x # After stage 1 the pivot block should be identical regardless of input form - assert rank == rank_s + assert rank == rank_ref assert np.array_equal( rref_synth.tableau.data[:, :rank], ref_synth.tableau.data[:, :rank], @@ -563,21 +548,18 @@ def test_replay_matches_final_state(self): replay.permute_columns(col_perm) # Replay all operations - for operation_type, payload in synth.circuit: + for operation_type, qubit_args in synth.circuit: if operation_type is MatrixCompressionType.CX: - replay.cx(*payload) + replay.cx(*qubit_args) elif operation_type is MatrixCompressionType.SWAP: - replay.swap(*payload) - elif operation_type is MatrixCompressionType.TOFFOLI: - tgt, ctrl_pos, ctrl_row, ctrl_val = payload - replay.toffoli(tgt, (ctrl_pos, True), (ctrl_row, ctrl_val)) + replay.swap(*qubit_args) + elif operation_type is MatrixCompressionType.CCX: + replay.toffoli(qubit_args[0], (qubit_args[1], True), (qubit_args[2], True)) elif operation_type is MatrixCompressionType.X: - replay.x(*payload) - elif operation_type is MatrixCompressionType.PUI_BLOCK: - fixed_controls, rest_entries = payload - replay.toffoli_pui_fixed(fixed_controls) - for off, ctrls in rest_entries: - replay.toffoli_pui_rest(off, ctrls) + replay.x(*qubit_args) + elif operation_type in {MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND}: + data_table, addr_qubits, dat_qubits = qubit_args + replay.select(data_table, addr_qubits, dat_qubits) # Undo column permutation inv_perm = [0] * len(col_perm) @@ -593,27 +575,35 @@ class TestToGf2xOperations: """Tests for operation export.""" def test_returns_ops_and_ancilla_count(self): - """to_gf2x_operations returns an op list.""" + """to_operations returns an op list.""" mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) synth = BinaryEncodingSynthesizer.from_matrix(mat) - ops = synth.to_gf2x_operations(num_local_qubits=3) + ops = synth.to_operations(num_local_qubits=3) assert isinstance(ops, list) - def test_op_names_are_strings(self): - """All emitted op names must belong to the known gate vocabulary.""" + def test_op_names_are_valid(self): + """All emitted op types must belong to the known gate vocabulary.""" mat = np.array([[1, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 0]], dtype=np.int8) synth = BinaryEncodingSynthesizer.from_matrix(mat) - ops = synth.to_gf2x_operations(num_local_qubits=3) - valid_names = {"cx", "swap", "ccx", "x", "mcx", "select", "select_and"} - for name, _ in ops: - assert name in valid_names, f"Unexpected op name: {name}" + ops = synth.to_operations(num_local_qubits=3) + valid_types = { + MatrixCompressionType.CX, + MatrixCompressionType.SWAP, + MatrixCompressionType.CCX, + MatrixCompressionType.X, + MatrixCompressionType.MCX, + MatrixCompressionType.SELECT, + MatrixCompressionType.SELECT_AND, + } + for op in ops: + assert op.name in valid_types, f"Unexpected op type: {op.name}" def test_translate_ops_identity_mapping(self): """When active_qubit_indices is identity, ops stay the same.""" mat = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 0]], dtype=np.int8) synth = BinaryEncodingSynthesizer.from_matrix(mat) - ops_raw = synth.to_gf2x_operations(num_local_qubits=3) - ops_xlat = synth.to_gf2x_operations( + ops_raw = synth.to_operations(num_local_qubits=3) + ops_xlat = synth.to_operations( num_local_qubits=3, active_qubit_indices=[0, 1, 2], ancilla_start=3, @@ -623,19 +613,19 @@ def test_translate_ops_identity_mapping(self): def test_translate_ops_remaps_indices(self): """Translation must remap qubit indices through the provided map.""" - ops = [("cx", (0, 1)), ("x", 2)] + ops = [(MatrixCompressionType.CX, (0, 1)), (MatrixCompressionType.X, 2)] translated = BinaryEncodingSynthesizer._translate_ops( ops, num_local_qubits=3, active_qubit_indices=[10, 20, 30], ancilla_start=100, ) - assert translated[0] == ("cx", (10, 20)) - assert translated[1] == ("x", 30) + assert translated[0] == (MatrixCompressionType.CX, (10, 20)) + assert translated[1] == (MatrixCompressionType.X, 30) def test_translate_ops_remaps_ancilla(self): """Indices >= num_local_qubits should map to ancilla space.""" - ops = [("cx", (0, 3))] + ops = [(MatrixCompressionType.CX, (0, 3))] translated = BinaryEncodingSynthesizer._translate_ops( ops, num_local_qubits=3, @@ -643,23 +633,23 @@ def test_translate_ops_remaps_ancilla(self): ancilla_start=100, ) # Index 3 >= num_local_qubits=3, so maps to ancilla_start + (3 - 3) = 100 - assert translated[0] == ("cx", (10, 100)) + assert translated[0] == (MatrixCompressionType.CX, (10, 100)) def test_translate_ops_ccx(self): """CCX indices are remapped through active_qubit_indices.""" - ops = [("ccx", (0, 1, 2))] + ops = [(MatrixCompressionType.CCX, (0, 1, 2))] translated = BinaryEncodingSynthesizer._translate_ops( ops, num_local_qubits=3, active_qubit_indices=[5, 6, 7], ancilla_start=10, ) - assert translated[0] == ("ccx", (5, 6, 7)) + assert translated[0] == (MatrixCompressionType.CCX, (5, 6, 7)) def test_translate_ops_select(self): """Select ops remap address and data qubit indices, keeping the table.""" data_table = [[True, False], [False, True]] - ops = [("select", (data_table, [0, 1], [2, 3]))] + ops = [(MatrixCompressionType.SELECT, (data_table, [0, 1], [2, 3]))] translated = BinaryEncodingSynthesizer._translate_ops( ops, num_local_qubits=4, @@ -673,7 +663,7 @@ def test_translate_ops_select(self): def test_translate_ops_mcx(self): """MCX remaps control and target indices, preserving control states.""" - ops = [("mcx", ([0, 1], [True, False], 2))] + ops = [(MatrixCompressionType.MCX, ([0, 1], [True, False], 2))] translated = BinaryEncodingSynthesizer._translate_ops( ops, num_local_qubits=3, @@ -692,10 +682,12 @@ def test_measurement_based_uses_select_and(self): dtype=np.int8, ) synth = BinaryEncodingSynthesizer.from_matrix(mat, measurement_based_uncompute=True) - ops = synth.to_gf2x_operations(num_local_qubits=4) - select_names = {name for name, _ in ops if "select" in name} - if select_names: - assert "select_and" in select_names + ops = synth.to_operations(num_local_qubits=4) + select_types = { + op.name for op in ops if op.name in {MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND} + } + if select_types: + assert MatrixCompressionType.SELECT_AND in select_types class TestLookupSelect: @@ -711,7 +703,7 @@ def test_single_entry(self): table = {(1,): (1,)} ops = _lookup_select(table, [0], [1]) assert len(ops) == 1 - assert ops[0][0] == "select" + assert ops[0][0] == MatrixCompressionType.SELECT def test_two_address_bits(self): """Two address bits produce a 2^2 = 4 entry dense data table.""" @@ -719,7 +711,7 @@ def test_two_address_bits(self): ops = _lookup_select(table, [0, 1], [2]) assert len(ops) == 1 name, (data_table, addr, dat) = ops[0] - assert name == "select" + assert name == MatrixCompressionType.SELECT assert addr == [0, 1] assert dat == [2] assert len(data_table) == 4 @@ -742,4 +734,4 @@ def test_select_and_mode(self): """use_measurement_and=True emits select_and instead of select.""" table = {(1,): (1,)} ops = _lookup_select(table, [0], [1], use_measurement_and=True) - assert ops[0][0] == "select_and" + assert ops[0][0] == MatrixCompressionType.SELECT_AND From 82bff65ef1561d34da0eba327d422c19e7e55a47 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Thu, 9 Apr 2026 20:34:01 +0000 Subject: [PATCH 10/21] fix StrEnum --- python/src/qdk_chemistry/data/circuit.py | 1 + .../qdk_chemistry/utils/binary_encoding.py | 47 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/python/src/qdk_chemistry/data/circuit.py b/python/src/qdk_chemistry/data/circuit.py index 9404b507b..7ff6d83de 100644 --- a/python/src/qdk_chemistry/data/circuit.py +++ b/python/src/qdk_chemistry/data/circuit.py @@ -187,6 +187,7 @@ def get_qsharp_circuit(self, prune_classical_qubits: bool = False) -> qsharp._na self._qsharp_factory.program, *self._qsharp_factory.parameter.values(), prune_classical_qubits=prune_classical_qubits, + generation_method=qsharp.CircuitGenerationMethod.Static, ) if self.qasm: return qsharp.openqasm.circuit(self.qasm) diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index d2c2b2280..b44bcffca 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -9,12 +9,11 @@ import math from dataclasses import dataclass, field -from enum import StrEnum -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import numpy as np -from qdk_chemistry.utils import Logger +from qdk_chemistry.utils import CaseInsensitiveStrEnum, Logger from qdk_chemistry.utils.qsharp import QSHARP_UTILS if TYPE_CHECKING: @@ -74,7 +73,7 @@ class NotRefError(ValueError): """Raised when a matrix is not in row echelon form (REF).""" -class MatrixCompressionType(StrEnum): +class MatrixCompressionType(CaseInsensitiveStrEnum): """Supported operation types for matrix compression.""" X = "X" @@ -377,7 +376,7 @@ def __init__( self.batch: list[_BatchElement] = [] self.batch_index: int = 0 - self.circuit: list[tuple[MatrixCompressionType, tuple[Any, ...]]] = [] + self.circuit: list[tuple[str, Any]] = [] self.bijection: list[tuple[int, int]] = [] self.bad_element_count: int = 0 @@ -440,7 +439,7 @@ def max_batch_size(self) -> int: return sparse_size return 1 << (sparse_size.bit_length() - 1) - def _record(self, op: tuple[MatrixCompressionType, tuple[Any, ...]]): + def _record(self, op: tuple[str, Any]): """Append an operation and update the tableau. Args: @@ -855,7 +854,7 @@ def _clear_sparse_bits(self): ] rest_entries.append((i, changing_controls)) - select_ops: list[tuple[MatrixCompressionType, Any]] = [] + select_ops: list[tuple[str, Any]] = [] self._flush_pui_lookup_block(select_ops, dense_size, fixed_controls, rest_entries) for op in select_ops: self._record(op) @@ -882,13 +881,14 @@ def to_operations( List of MatrixCompressionOp. """ - raw_ops: list[tuple[MatrixCompressionType, Any]] = [] + raw_ops: list[tuple[str, Any]] = [] for compress_type, qubit_args in self.circuit: - if compress_type is MatrixCompressionType.X: - raw_ops.append((compress_type, qubit_args[0])) + op_type = MatrixCompressionType(compress_type) + if op_type is MatrixCompressionType.X: + raw_ops.append((op_type, qubit_args[0])) else: - raw_ops.append((compress_type, qubit_args)) + raw_ops.append((op_type, qubit_args)) if active_qubit_indices is not None and ancilla_start is not None: raw_ops = self._translate_ops(raw_ops, num_local_qubits, active_qubit_indices, ancilla_start) @@ -899,7 +899,7 @@ def to_operations( return ops @staticmethod - def _to_compression_op(op_type: MatrixCompressionType, op_args: Any) -> MatrixCompressionOp: + def _to_compression_op(op_type: str, op_args: Any) -> MatrixCompressionOp: """Convert a raw circuit tuple into a MatrixCompressionOp. Args: @@ -910,6 +910,7 @@ def _to_compression_op(op_type: MatrixCompressionType, op_args: Any) -> MatrixCo A MatrixCompressionOp instance. """ + op_type = MatrixCompressionType(op_type) if op_type is MatrixCompressionType.X: return MatrixCompressionOp(op_type, [int(op_args)]) if op_type in {MatrixCompressionType.CX, MatrixCompressionType.SWAP}: @@ -929,11 +930,11 @@ def _to_compression_op(op_type: MatrixCompressionType, op_args: Any) -> MatrixCo @staticmethod def _translate_ops( - ops: list[tuple[MatrixCompressionType, Any]], + ops: list[tuple[str, Any]], num_local_qubits: int, active_qubit_indices: list[int], ancilla_start: int, - ) -> list[tuple[MatrixCompressionType, Any]]: + ) -> list[tuple[str, Any]]: """Remap local qubit indices to global topological indices. Indices below ``num_local_qubits`` are mapped through @@ -956,8 +957,9 @@ def map_idx(idx: int) -> int: int(active_qubit_indices[idx]) if idx < num_local_qubits else ancilla_start + (idx - num_local_qubits) ) - translated: list[tuple[MatrixCompressionType, Any]] = [] - for op_type, op_args in ops: + translated: list[tuple[str, Any]] = [] + for compress_type, op_args in ops: + op_type = MatrixCompressionType(compress_type) if op_type is MatrixCompressionType.X: translated.append((MatrixCompressionType.X, map_idx(int(op_args)))) elif op_type in {MatrixCompressionType.CX, MatrixCompressionType.SWAP}: @@ -994,7 +996,7 @@ def map_idx(idx: int) -> int: def _flush_pui_lookup_block( self, - ops: list[tuple[MatrixCompressionType, Any]], + ops: list[tuple[str, Any]], sbs: int, fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], @@ -1043,7 +1045,7 @@ def _synthesize_single_pui_lookup_block( sbs: int, fixed_controls: list[tuple[int, bool]], rest_entries: list[tuple[int, list[tuple[int, bool]]]], - ) -> tuple[list[tuple[MatrixCompressionType, Any]], int]: + ) -> tuple[list[tuple[str, Any]], int]: """Lower one PUI sub-block into lookup ops. Args: @@ -1074,9 +1076,8 @@ def _synthesize_single_pui_lookup_block( use_measurement_and=self.measurement_based_uncompute, ) - typed_lookup_ops = cast("list[tuple[MatrixCompressionType, Any]]", lookup_ops) - gf2x_ops = list(reversed(typed_lookup_ops)) - and_count = sum(1 for name, _ in typed_lookup_ops if name == "and") + gf2x_ops = list(reversed(lookup_ops)) + and_count = sum(1 for name, _ in lookup_ops if name == "and") return gf2x_ops, and_count @@ -1211,7 +1212,7 @@ def _lookup_select( data_qubits: list[int], *, use_measurement_and: bool = False, -) -> list[tuple[MatrixCompressionType, Any]]: +) -> list[tuple[str, Any]]: """Synthesize a lookup-based select or select_and operation for a given truth table. Args: @@ -1230,7 +1231,7 @@ def _lookup_select( if not table_dict: return [] - operations: list[tuple[MatrixCompressionType, Any]] = [] + operations: list[tuple[str, Any]] = [] n_address = len(address_qubits) n_data = len(data_qubits) From 8c10b63f38f76e026b67489dedfa8449002b8af5 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Thu, 9 Apr 2026 22:01:26 +0000 Subject: [PATCH 11/21] static not compatible with prune qubits --- python/src/qdk_chemistry/data/circuit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/src/qdk_chemistry/data/circuit.py b/python/src/qdk_chemistry/data/circuit.py index 7ff6d83de..9404b507b 100644 --- a/python/src/qdk_chemistry/data/circuit.py +++ b/python/src/qdk_chemistry/data/circuit.py @@ -187,7 +187,6 @@ def get_qsharp_circuit(self, prune_classical_qubits: bool = False) -> qsharp._na self._qsharp_factory.program, *self._qsharp_factory.parameter.values(), prune_classical_qubits=prune_classical_qubits, - generation_method=qsharp.CircuitGenerationMethod.Static, ) if self.qasm: return qsharp.openqasm.circuit(self.qasm) From 1bfe50931befef58fc7788badf5d98727f3f6a06 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Fri, 10 Apr 2026 17:22:18 +0000 Subject: [PATCH 12/21] apply AI code review comments --- .../src/qdk_chemistry/utils/binary_encoding.py | 16 ++++++++++------ .../src/qdk_chemistry/utils/qsharp/__init__.py | 3 +-- .../utils/qsharp/src/BinaryEncoding.qs | 6 +++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index b44bcffca..8c1462cae 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -102,8 +102,8 @@ class MatrixCompressionOp: def __post_init__(self): """Validate the MatrixCompressionOp parameters.""" - if self.name == MatrixCompressionType.SELECT and not self.lookup_data: - raise ValueError("lookup_data must be provided for SELECT operations") + if self.name in {MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND} and not self.lookup_data: + raise ValueError(f"lookup_data must be provided for {self.name} operations") def to_dict(self) -> dict[str, Any]: """Serialize to a camelCase dict matching the Q# ``MatrixCompressionOp`` struct.""" @@ -178,13 +178,17 @@ def __init__(self, data: np.ndarray): """ self.data = np.asarray(data, dtype=np.int8) - assert self.data.ndim == 2 + if self.data.ndim != 2: + raise ValueError("Input data must be a 2-dimensional array") _check_ref(self.data) self.num_rows, self.num_cols = self.data.shape self.dense_size = _dense_qubits_size(self.num_cols) - assert self.dense_size < self.num_rows + if self.dense_size >= self.num_rows: + raise ValueError( + f"Dense size ({self.dense_size}) must be strictly less than number of rows ({self.num_rows})" + ) self._tmp_row = np.zeros(self.num_cols, dtype=np.int8) self.pivots = self.identify_pivots() @@ -923,9 +927,9 @@ def _to_compression_op(op_type: str, op_args: Any) -> MatrixCompressionOp: qubits = [int(q) for q in addr_qubits] + [int(q) for q in dat_qubits] return MatrixCompressionOp(op_type, qubits, control_state=len(addr_qubits), lookup_data=data_table) if op_type is MatrixCompressionType.MCX: - controls, _, target_qubit = op_args + controls, control_state, target_qubit = op_args qubits = [int(q) for q in controls] + [int(target_qubit)] - return MatrixCompressionOp(op_type, qubits, control_state=len(controls)) + return MatrixCompressionOp(op_type, qubits, control_state=control_state) raise ValueError(f"Unknown op type: {op_type}") @staticmethod diff --git a/python/src/qdk_chemistry/utils/qsharp/__init__.py b/python/src/qdk_chemistry/utils/qsharp/__init__.py index 22cc0be96..cf5ca18c0 100644 --- a/python/src/qdk_chemistry/utils/qsharp/__init__.py +++ b/python/src/qdk_chemistry/utils/qsharp/__init__.py @@ -7,7 +7,6 @@ from pathlib import Path import qdk -import qsharp from qdk import init as qdk_init from qsharp._qsharp import get_config as get_qdk_profile_config @@ -19,7 +18,7 @@ # Ensure the Q# interpreter uses Adaptive_RIF qdk_config = get_qdk_profile_config() -_target_profile = qsharp.TargetProfile.Adaptive_RIF +_target_profile = qdk.TargetProfile.Adaptive_RIF if qdk_config.get_target_profile() != "adaptive_rif": Logger.debug( diff --git a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs index 6cd1a7485..064cff5ac 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs @@ -21,7 +21,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { /// ("CX", [control, target], 0, []) /// ("SWAP", [a, b], 0, []) /// ("CCX", [ctrl1, ctrl2, target], 0, []) - /// ("MCX", [target, ctrl0, ctrl1, ...], ctrlStateBitmask, []) + /// ("MCX", [ctrl0, ctrl1, ..., target], ctrlStateBitmask, []) /// ("SELECT", [addr0..addrN, data0..dataM], numAddrQubits, data[][]) struct MatrixCompressionOp { name : String, @@ -55,12 +55,12 @@ namespace QDKChemistry.Utils.BinaryEncoding { } elif gate.name == "CCX" { CCNOT(qs[gate.qubits[0]], qs[gate.qubits[1]], qs[gate.qubits[2]]); } elif gate.name == "MCX" { - let target = gate.qubits[0]; let numControls = Length(gate.qubits) - 1; + let target = gate.qubits[numControls]; mutable controlQubits = []; mutable ctrlStateBools = []; for i in 0..numControls - 1 { - set controlQubits += [qs[gate.qubits[1 + i]]]; + set controlQubits += [qs[gate.qubits[i]]]; set ctrlStateBools += [((gate.controlState >>> i) &&& 1) == 1]; } ApplyControlledOnBitString(ctrlStateBools, X, controlQubits, qs[target]); From 3c1764e57b9b8dd9cfec84f79f61b2d9d38b7f26 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Thu, 23 Apr 2026 18:46:22 +0000 Subject: [PATCH 13/21] use ancilla from idle qubit pools --- .../sparse_isometry_binary_encoding.py | 11 +- .../utils/qsharp/src/BinaryEncoding.qs | 178 +++++++++++++----- .../utils/qsharp/src/StatePreparation.qs | 2 +- .../test_state_preparation_binary_encoding.py | 30 ++- 4 files changed, 161 insertions(+), 60 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index 4b1225a84..cff6293fb 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -143,12 +143,20 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: # PreparePureStateD treats qubits[0] as MSB, so pass dense_row_map # as-is (row 0 first) — do NOT reverse like the parent sparse isometry # (which uses the opposite convention: row rank-1 = MSB). + # Pool A only: qubits outside the active set entirely — never touched by binary encoding. + # Pool B (idle batch rows) is intentionally excluded: the reversed GF2X correction circuit + # applies CX gates AFTER SELECT, which entangles those rows with the helper qubit and + # corrupts fidelity. + active_qubits_set = {int(q) for q in gf2x_result.row_map} + ancilla_pool = sorted(set(range(n_qubits)) - active_qubits_set) + state_prep_params = QSHARP_UTILS.BinaryEncoding.BinaryEncodingStatePreparationParams( rowMap=list(dense_row_map), stateVector=compressed_sv.tolist(), gaussianEliminationOps=[op.to_qsharp_parameter() for op in gaussian_elimination_ops], binaryEncodingOps=[op.to_qsharp_parameter() for op in encoded_ops], numQubits=n_qubits, + ancillaPool=ancilla_pool, ) qsharp_factory = QsharpFactoryData( @@ -158,7 +166,8 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: qsharp_op = QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationOp(*vars(state_prep_params).values()) Logger.info( f"Binary encoding produced {len(encoded_ops)} operations " - f"for {n_qubits}-qubit system with {len(bitstrings)} determinants" + f"for {n_qubits}-qubit system with {len(bitstrings)} determinants " + f"using {len(ancilla_pool)} pre-existing qubits as ancilla pool" ) return Circuit( diff --git a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs index 064cff5ac..ec7de8312 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs @@ -42,10 +42,17 @@ namespace QDKChemistry.Utils.BinaryEncoding { binaryEncodingOps : MatrixCompressionOp[], /// Total number of qubits. numQubits : Int, + /// Qubit indices (into the main register) that are idle during binary encoding + /// and can be borrowed as ancillas by SparseOneHotSelect. + ancillaPool : Int[], } /// Apply a single matrix-compression gate to a qubit register. - operation ApplyMatrixCompressionOp(gate : MatrixCompressionOp, qs : Qubit[]) : Unit { + /// + /// ``ancillaPool`` is a list of pre-initialised |0⟩ qubits that + /// SparseOneHotSelect may borrow as helpers (avoids allocating new qubits). + /// Pass an empty array when no pool is available (e.g. for GF2+X ops). + operation ApplyMatrixCompressionOp(gate : MatrixCompressionOp, qs : Qubit[], ancillaPool : Qubit[]) : Unit { if gate.name == "X" { X(qs[gate.qubits[0]]); } elif gate.name == "CX" { @@ -75,7 +82,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { set targetQubits += [qs[gate.qubits[i]]]; } } - SparseOneHotSelect(gate.lookupData, addrQubits, targetQubits, false); + SparseOneHotSelect(gate.lookupData, addrQubits, targetQubits, false, ancillaPool); } elif gate.name == "SELECT_AND" { let numAddr = gate.controlState; mutable addrQubits : Qubit[] = []; @@ -87,7 +94,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { set targetQubits += [qs[gate.qubits[i]]]; } } - SparseOneHotSelect(gate.lookupData, addrQubits, targetQubits, true); + SparseOneHotSelect(gate.lookupData, addrQubits, targetQubits, true, ancillaPool); } else { fail $"Unknown gate name: {gate.name}"; } @@ -130,14 +137,21 @@ namespace QDKChemistry.Utils.BinaryEncoding { } } - /// Sparse one-hot select + /// Sparse one-hot select. + /// + /// For each row of ``data``, applies X to the target bits where the row is + /// true, controlled on the address qubits matching that row's index. /// - /// For each row of the data, apply X to the target bits where the row is true, controlled on the address qubits being in the state corresponding to that row. + /// ``ancillaPool`` is a list of pre-initialised |0⟩ qubits that the + /// recursive helper may borrow instead of allocating new ones. Each + /// borrowed qubit is restored to |0⟩ before the operation returns. + /// Pass an empty array to fall back to ``use`` allocation. operation SparseOneHotSelect( data : Bool[][], address : Qubit[], target : Qubit[], - useMeasurementAND : Bool + useMeasurementAND : Bool, + ancillaPool : Qubit[] ) : Unit { let N = Length(data); @@ -154,14 +168,14 @@ namespace QDKChemistry.Utils.BinaryEncoding { if not leftEmpty and not rightEmpty { within { X(tail); } apply { - SparseOneHotSCS(tail, parts[0], most, target, useMeasurementAND); + SparseOneHotSCS(tail, parts[0], most, target, useMeasurementAND, ancillaPool); } - SparseOneHotSCS(tail, parts[1], most, target, useMeasurementAND); + SparseOneHotSCS(tail, parts[1], most, target, useMeasurementAND, ancillaPool); } elif not rightEmpty { - SparseOneHotSCS(tail, parts[1], most, target, useMeasurementAND); + SparseOneHotSCS(tail, parts[1], most, target, useMeasurementAND, ancillaPool); } elif not leftEmpty { within { X(tail); } apply { - SparseOneHotSCS(tail, parts[0], most, target, useMeasurementAND); + SparseOneHotSCS(tail, parts[0], most, target, useMeasurementAND, ancillaPool); } } } @@ -169,13 +183,16 @@ namespace QDKChemistry.Utils.BinaryEncoding { /// Singly-controlled recursion for SparseOneHotSelect. /// - /// When ``useMeasurementAND`` is true, uses MeasurementBasedAND + Adjoint + /// Uses ``ancillaPool[0]`` as the helper qubit (must be |0⟩ on entry, + /// restored on exit) and passes ``ancillaPool[1...]`` to recursive calls. + /// Falls back to ``use helper = Qubit()`` when the pool is empty. operation SparseOneHotSCS( ctl : Qubit, data : Bool[][], address : Qubit[], target : Qubit[], - useMeasurementAND : Bool + useMeasurementAND : Bool, + ancillaPool : Qubit[] ) : Unit { let N = Length(data); @@ -189,51 +206,106 @@ namespace QDKChemistry.Utils.BinaryEncoding { let parts = Partitioned([2^(n - 1)], data); let leftEmpty = IsDataAllZeros(parts[0]); let rightEmpty = IsDataAllZeros(parts[1]); + let poolLen = Length(ancillaPool); if not leftEmpty and not rightEmpty { - use helper = Qubit(); - if useMeasurementAND { - within { X(tail); } apply { - MeasurementBasedAND(ctl, tail, helper); + if poolLen > 0 { + let helper = ancillaPool[0]; + let restPool = ancillaPool[1...]; + if useMeasurementAND { + within { X(tail); } apply { + MeasurementBasedAND(ctl, tail, helper); + } + SparseOneHotSCS(helper, parts[0], most, target, true, restPool); + CNOT(ctl, helper); + SparseOneHotSCS(helper, parts[1], most, target, true, restPool); + Adjoint MeasurementBasedAND(ctl, tail, helper); + } else { + within { X(tail); } apply { + CCNOT(ctl, tail, helper); + } + SparseOneHotSCS(helper, parts[0], most, target, false, restPool); + CNOT(ctl, helper); + SparseOneHotSCS(helper, parts[1], most, target, false, restPool); + CCNOT(ctl, tail, helper); } - SparseOneHotSCS(helper, parts[0], most, target, true); - CNOT(ctl, helper); - SparseOneHotSCS(helper, parts[1], most, target, true); - Adjoint MeasurementBasedAND(ctl, tail, helper); } else { - within { X(tail); } apply { + use helper = Qubit(); + if useMeasurementAND { + within { X(tail); } apply { + MeasurementBasedAND(ctl, tail, helper); + } + SparseOneHotSCS(helper, parts[0], most, target, true, []); + CNOT(ctl, helper); + SparseOneHotSCS(helper, parts[1], most, target, true, []); + Adjoint MeasurementBasedAND(ctl, tail, helper); + } else { + within { X(tail); } apply { + CCNOT(ctl, tail, helper); + } + SparseOneHotSCS(helper, parts[0], most, target, false, []); + CNOT(ctl, helper); + SparseOneHotSCS(helper, parts[1], most, target, false, []); CCNOT(ctl, tail, helper); } - SparseOneHotSCS(helper, parts[0], most, target, false); - CNOT(ctl, helper); - SparseOneHotSCS(helper, parts[1], most, target, false); - CCNOT(ctl, tail, helper); } } elif not rightEmpty { - use helper = Qubit(); - if useMeasurementAND { - MeasurementBasedAND(ctl, tail, helper); - SparseOneHotSCS(helper, parts[1], most, target, true); - Adjoint MeasurementBasedAND(ctl, tail, helper); + if poolLen > 0 { + let helper = ancillaPool[0]; + let restPool = ancillaPool[1...]; + if useMeasurementAND { + MeasurementBasedAND(ctl, tail, helper); + SparseOneHotSCS(helper, parts[1], most, target, true, restPool); + Adjoint MeasurementBasedAND(ctl, tail, helper); + } else { + CCNOT(ctl, tail, helper); + SparseOneHotSCS(helper, parts[1], most, target, false, restPool); + CCNOT(ctl, tail, helper); + } } else { - CCNOT(ctl, tail, helper); - SparseOneHotSCS(helper, parts[1], most, target, false); - CCNOT(ctl, tail, helper); + use helper = Qubit(); + if useMeasurementAND { + MeasurementBasedAND(ctl, tail, helper); + SparseOneHotSCS(helper, parts[1], most, target, true, []); + Adjoint MeasurementBasedAND(ctl, tail, helper); + } else { + CCNOT(ctl, tail, helper); + SparseOneHotSCS(helper, parts[1], most, target, false, []); + CCNOT(ctl, tail, helper); + } } } elif not leftEmpty { - use helper = Qubit(); - if useMeasurementAND { - X(tail); - MeasurementBasedAND(ctl, tail, helper); - SparseOneHotSCS(helper, parts[0], most, target, true); - Adjoint MeasurementBasedAND(ctl, tail, helper); - X(tail); + if poolLen > 0 { + let helper = ancillaPool[0]; + let restPool = ancillaPool[1...]; + if useMeasurementAND { + X(tail); + MeasurementBasedAND(ctl, tail, helper); + SparseOneHotSCS(helper, parts[0], most, target, true, restPool); + Adjoint MeasurementBasedAND(ctl, tail, helper); + X(tail); + } else { + X(tail); + CCNOT(ctl, tail, helper); + SparseOneHotSCS(helper, parts[0], most, target, false, restPool); + CCNOT(ctl, tail, helper); + X(tail); + } } else { - X(tail); - CCNOT(ctl, tail, helper); - SparseOneHotSCS(helper, parts[0], most, target, false); - CCNOT(ctl, tail, helper); - X(tail); + use helper = Qubit(); + if useMeasurementAND { + X(tail); + MeasurementBasedAND(ctl, tail, helper); + SparseOneHotSCS(helper, parts[0], most, target, true, []); + Adjoint MeasurementBasedAND(ctl, tail, helper); + X(tail); + } else { + X(tail); + CCNOT(ctl, tail, helper); + SparseOneHotSCS(helper, parts[0], most, target, false, []); + CCNOT(ctl, tail, helper); + X(tail); + } } } } @@ -244,6 +316,9 @@ namespace QDKChemistry.Utils.BinaryEncoding { /// The procedure is: /// 1. Prepare the dense statevector on the reduced qubit subset. /// 2. Apply the binary-encoding operations (already reversed by Python). + /// SELECT gates borrow ancillas from ``params.ancillaPool`` (qubits that + /// are idle during binary encoding and start in |0⟩) to avoid allocating + /// extra qubits. /// 3. Apply the GF2+X expansion operations (CX / X). operation BinaryEncodingStatePreparation( params : BinaryEncodingStatePreparationParams, @@ -252,14 +327,17 @@ namespace QDKChemistry.Utils.BinaryEncoding { // Step 1: Dense state prep on reduced subspace PreparePureStateD(params.stateVector, Subarray(params.rowMap, qs)); + // Build ancilla pool from indices + let poolQubits = Subarray(params.ancillaPool, qs); + // Step 2: Apply binary-encoding ops (pre-reversed by Python) for gate in params.binaryEncodingOps { - ApplyMatrixCompressionOp(gate, qs); + ApplyMatrixCompressionOp(gate, qs, poolQubits); } - // Step 3: Expand back via GF2+X operations + // Step 3: Expand back via GF2+X operations (CX/X only — no pool needed) for gate in params.gaussianEliminationOps { - ApplyMatrixCompressionOp(gate, qs); + ApplyMatrixCompressionOp(gate, qs, []); } } @@ -270,6 +348,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { gaussianEliminationOps : MatrixCompressionOp[], binaryEncodingOps : MatrixCompressionOp[], numQubits : Int, + ancillaPool : Int[], ) : Qubit[] => Unit { BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { rowMap = rowMap, @@ -277,6 +356,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { gaussianEliminationOps = gaussianEliminationOps, binaryEncodingOps = binaryEncodingOps, numQubits = numQubits, + ancillaPool = ancillaPool, }, _) } @@ -287,6 +367,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { gaussianEliminationOps : MatrixCompressionOp[], binaryEncodingOps : MatrixCompressionOp[], numQubits : Int, + ancillaPool : Int[], ) : Unit { use qs = Qubit[numQubits]; BinaryEncodingStatePreparation(new BinaryEncodingStatePreparationParams { @@ -295,6 +376,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { gaussianEliminationOps = gaussianEliminationOps, binaryEncodingOps = binaryEncodingOps, numQubits = numQubits, + ancillaPool = ancillaPool, }, qs); } } diff --git a/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs b/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs index d9e3d50d6..687a4f18d 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs @@ -38,7 +38,7 @@ namespace QDKChemistry.Utils.StatePreparation { ) : Unit { PreparePureStateD(params.stateVector, Subarray(params.rowMap, qs)); for gate in params.expansionOps { - ApplyMatrixCompressionOp(gate, qs); + ApplyMatrixCompressionOp(gate, qs, []); } } diff --git a/python/tests/test_state_preparation_binary_encoding.py b/python/tests/test_state_preparation_binary_encoding.py index a2aed23d0..507cc637d 100644 --- a/python/tests/test_state_preparation_binary_encoding.py +++ b/python/tests/test_state_preparation_binary_encoding.py @@ -21,13 +21,14 @@ def _matrix_qubit_counts(wf: Wavefunction) -> tuple[int, int]: - """Derive qubit counts from the determinant matrix. + """Derive qubit counts from the determinant matrix, accounting for the ancilla pool. Returns: ``(n_system, n_ancilla)`` where - *n_system* is the number of system qubits - - *n_ancilla* is the number of ancilla qubits. + - *n_ancilla* is the number of extra ancilla qubits beyond the system register + after subtracting Pool A (idle GF2X qubits: system qubits absent from row_map). """ num_orbitals = len(wf.get_orbitals().get_active_space_indices()[0]) @@ -47,13 +48,22 @@ def _matrix_qubit_counts(wf: Wavefunction) -> tuple[int, int]: active_qubit_indices=gf2x_result.row_map, ancilla_start=n_system, ) - max_select_ancilla = 0 - for op in ops: - if op.name in (MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND): - n_addr = op.control_state - max_select_ancilla = max(max_select_ancilla, n_addr - 1) + naive_ancilla = max( + ( + op.control_state - 1 + for op in ops + if op.name in (MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND) + ), + default=0, + ) + + # Pool A: system qubits not present in row_map (never touched by binary encoding ops) + active_set = {int(q) for q in gf2x_result.row_map} + pool_a = len(set(range(n_system)) - active_set) - return n_system, max_select_ancilla + # Actual ancilla = max(0, naive - pool_a) + actual_ancilla = max(0, naive_ancilla - pool_a) + return n_system, actual_ancilla @pytest.fixture @@ -75,7 +85,7 @@ def test_ozone(self, ozone_wf): result = circuit.estimate() assert isinstance(result, qsharp.estimator.EstimatorResult) lc = result["logicalCounts"] - assert lc["numQubits"] == 12 # 10 system qubits + 2 ancilla qubits + assert lc["numQubits"] == 10 # 10 system qubits; pool covers all ancilla assert lc["tCount"] == 7 assert lc["rotationCount"] == 7 assert lc["cczCount"] == 9 @@ -159,7 +169,7 @@ def test_ozone_negative_controls_disabled(self, ozone_wf): circuit = prep.run(ozone_wf) assert isinstance(circuit, Circuit) lc = circuit.estimate()["logicalCounts"] - assert lc["numQubits"] == 11 + assert lc["numQubits"] == 10 # 10 system qubits; pool covers the 1 ancilla assert lc["tCount"] == 7 assert lc["rotationCount"] == 7 assert lc["cczCount"] == 5 From 1482d91c1a2d470ae879ba8f47d56f2ea0cb9884 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Thu, 23 Apr 2026 22:41:06 +0000 Subject: [PATCH 14/21] remove unused ancilla pool comments --- .../sparse_isometry_binary_encoding.py | 6 ++--- .../qdk_chemistry/utils/binary_encoding.py | 22 ++++++++++--------- python/tests/test_utils_binary_encoding.py | 8 ++++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index cff6293fb..0d55469b9 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -143,10 +143,8 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: # PreparePureStateD treats qubits[0] as MSB, so pass dense_row_map # as-is (row 0 first) — do NOT reverse like the parent sparse isometry # (which uses the opposite convention: row rank-1 = MSB). - # Pool A only: qubits outside the active set entirely — never touched by binary encoding. - # Pool B (idle batch rows) is intentionally excluded: the reversed GF2X correction circuit - # applies CX gates AFTER SELECT, which entangles those rows with the helper qubit and - # corrupts fidelity. + # Create the ancilla pool from the original qubits that are not touched by binary encoding (i.e. not in row_map) + # since they are idle until the expansion stage and can be borrowed as ancillas during SparseOneHotSCS. active_qubits_set = {int(q) for q in gf2x_result.row_map} ancilla_pool = sorted(set(range(n_qubits)) - active_qubits_set) diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index 8c1462cae..e9274757e 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -977,7 +977,7 @@ def map_idx(idx: int) -> int: MatrixCompressionType.MCX, ( [map_idx(int(q)) for q in controls], - list(ctrl_state), + ctrl_state, map_idx(int(target)), ), ) @@ -1017,7 +1017,7 @@ def _flush_pui_lookup_block( if not rest_entries: return - mono_ops, mono_and = self._synthesize_single_pui_lookup_block( + mono_ops, mono_count = self._synthesize_single_pui_lookup_block( sbs, fixed_controls, rest_entries, @@ -1028,17 +1028,17 @@ def _flush_pui_lookup_block( ops.extend(mono_ops) return - chunked_ops, chunked_and = [], 0 + chunked_ops, chunked_count = [], 0 for chunk in chunked: - sub_ops, sub_and = self._synthesize_single_pui_lookup_block( + sub_ops, sub_count = self._synthesize_single_pui_lookup_block( sbs, fixed_controls, chunk, ) chunked_ops.extend(sub_ops) - chunked_and += sub_and + chunked_count += sub_count - if chunked_and <= mono_and: + if chunked_count <= mono_count: ops.extend(chunked_ops) return @@ -1058,8 +1058,8 @@ def _synthesize_single_pui_lookup_block( rest_entries: Per-target offsets and changing controls. Returns: - ``(ops, and_count)`` where ``and_count`` is the number of - emitted ``and`` operations, used as a Toffoli-cost proxy. + ``(ops, select_count)`` where ``select_count`` is the number of + emitted SELECT/SELECT_AND operations, used as a cost proxy. """ if not rest_entries: @@ -1081,9 +1081,11 @@ def _synthesize_single_pui_lookup_block( ) gf2x_ops = list(reversed(lookup_ops)) - and_count = sum(1 for name, _ in lookup_ops if name == "and") + select_count = sum( + 1 for name, _ in lookup_ops if name in (MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND) + ) - return gf2x_ops, and_count + return gf2x_ops, select_count def _canonicalize_pui_controls( self, diff --git a/python/tests/test_utils_binary_encoding.py b/python/tests/test_utils_binary_encoding.py index d85ab9886..eae3e79e7 100644 --- a/python/tests/test_utils_binary_encoding.py +++ b/python/tests/test_utils_binary_encoding.py @@ -662,8 +662,9 @@ def test_translate_ops_select(self): assert dat == [12, 13] def test_translate_ops_mcx(self): - """MCX remaps control and target indices, preserving control states.""" - ops = [(MatrixCompressionType.MCX, ([0, 1], [True, False], 2))] + """MCX remaps control and target indices; ctrl_state int bitmask passes through unchanged.""" + # ctrl_state is always an int bitmask (0b01 = first control active, second inactive) + ops = [(MatrixCompressionType.MCX, ([0, 1], 0b01, 2))] translated = BinaryEncodingSynthesizer._translate_ops( ops, num_local_qubits=3, @@ -672,7 +673,8 @@ def test_translate_ops_mcx(self): ) _, (ctrls, state, tgt) = translated[0] assert ctrls == [5, 6] - assert state == [True, False] + assert state == 0b01 + assert isinstance(state, int) assert tgt == 7 def test_measurement_based_uses_select_and(self): From 85891638457ba43a3fc8e7e55f170ad60105d169 Mon Sep 17 00:00:00 2001 From: yingrongchen Date: Fri, 24 Apr 2026 23:12:38 +0000 Subject: [PATCH 15/21] add methods to create dense and sparse isometry circuit separately Co-authored-by: Copilot --- .../state_preparation/sparse_isometry.py | 64 +++++++++++++- .../sparse_isometry_binary_encoding.py | 84 ++++++++++++++++--- .../utils/qsharp/src/BinaryEncoding.qs | 47 +++++++---- .../utils/qsharp/src/StatePreparation.qs | 44 +++++++++- 4 files changed, 205 insertions(+), 34 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index 6f39f2a15..3071af3a9 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -124,6 +124,30 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: "alpha and beta orbitals are not supported for state preparation." ) + params = self._build_state_prep_params(wavefunction) + + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.StatePreparation.MakeStatePreparationCircuit, + parameter=params, + ) + + state_prep_params = QSHARP_UTILS.StatePreparation.StatePreparationParams(**params) + state_prep_op = QSHARP_UTILS.StatePreparation.MakeStatePreparationOp(state_prep_params) + return Circuit(qsharp_factory=qsharp_factory, qsharp_op=state_prep_op, encoding="jordan-wigner") + + def _build_state_prep_params(self, wavefunction: Wavefunction) -> dict: + """Build state preparation parameters from a wavefunction. + + Extracts coefficients and determinants, performs GF2+X elimination, + and returns the parameter dict for Q# circuit construction. + + Args: + wavefunction: The target wavefunction to prepare. + + Returns: + A parameter dict for Q# circuit construction. + + """ coeffs = wavefunction.get_coefficients() dets = wavefunction.get_active_determinants() num_orbitals = len(wavefunction.get_orbitals().get_active_space_indices()[0]) @@ -166,14 +190,46 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: expansionOps=[op.to_dict() for op in expansion_ops], numQubits=n_qubits, ) + return vars(state_prep_params) + + def _create_dense(self, params: dict) -> Circuit: + """Create a standalone dense state preparation circuit. + Args: + params: The parameter dict for Q# circuit construction. + + Returns: + A dense state preparation circuit on the reduced qubit subset. + + """ qsharp_factory = QsharpFactoryData( - program=QSHARP_UTILS.StatePreparation.MakeStatePreparationCircuit, - parameter=vars(state_prep_params), + program=QSHARP_UTILS.StatePreparation.MakeDenseStatePreparation, + parameter={ + "rowMap": params["rowMap"], + "stateVector": params["stateVector"], + "numQubits": params["numQubits"], + }, ) + return Circuit(qsharp_factory=qsharp_factory, encoding="jordan-wigner") - state_prep_op = QSHARP_UTILS.StatePreparation.MakeStatePreparationOp(state_prep_params) - return Circuit(qsharp_factory=qsharp_factory, qsharp_op=state_prep_op, encoding="jordan-wigner") + def _create_isometry(self, params: dict) -> Circuit: + """Create a standalone isometry circuit. + + Args: + params: The parameter dict for Q# circuit construction. + + Returns: + A Circuit containing the GF2+X expansion operations. + + """ + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.StatePreparation.MakeExpansion, + parameter={ + "expansionOps": params["expansionOps"], + "numQubits": params["numQubits"], + }, + ) + return Circuit(qsharp_factory=qsharp_factory, encoding="jordan-wigner") def _qiskit_dense_preparation( self, gf2x_operation_results: "GF2XEliminationResult", statevector_data: np.ndarray, num_qubits: int diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index 0d55469b9..1a83076fe 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -89,6 +89,40 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: """ Logger.trace_entering() + + params = self._build_binary_encoding_params(wavefunction) + + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationCircuit, + parameter=params, + ) + qsharp_op = QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationOp(*params.values()) + Logger.info( + f"Binary encoding produced {len(params['binaryEncodingOps'])} operations " + f"for {n_qubits}-qubit system with {len(bitstrings)} determinants " + f"using {len(params['ancillaPool'])} pre-existing qubits as ancilla pool" + ) + + return Circuit( + qsharp_factory=qsharp_factory, + qsharp_op=qsharp_op, + encoding="jordan-wigner", + ) + + def _build_binary_encoding_params(self, wavefunction: Wavefunction) -> dict: + """Build binary-encoding state preparation parameters from a wavefunction. + + Extracts coefficients and determinants, performs GF2+X elimination and + binary-encoding synthesis, and returns the parameter dict for Q# circuit + construction. + + Args: + wavefunction: The target wavefunction to prepare. + + Returns: + A dict of parameters for Q# circuit construction. + + """ coeffs = wavefunction.get_coefficients() dets = wavefunction.get_active_determinants() num_orbitals = len(wavefunction.get_orbitals().get_active_space_indices()[0]) @@ -156,23 +190,49 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: numQubits=n_qubits, ancillaPool=ancilla_pool, ) + return vars(state_prep_params) + + def _create_dense(self, params: dict) -> Circuit: + """Create a standalone dense state preparation circuit. + + Args: + params: The parameter dict for Q# circuit construction. + + Returns: + A dense state preparation circuit on the reduced qubit subset. + """ qsharp_factory = QsharpFactoryData( - program=QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationCircuit, - parameter=vars(state_prep_params), - ) - qsharp_op = QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationOp(*vars(state_prep_params).values()) - Logger.info( - f"Binary encoding produced {len(encoded_ops)} operations " - f"for {n_qubits}-qubit system with {len(bitstrings)} determinants " - f"using {len(ancilla_pool)} pre-existing qubits as ancilla pool" + program=QSHARP_UTILS.StatePreparation.MakeDenseStatePreparation, + parameter={ + "rowMap": params["rowMap"], + "stateVector": params["stateVector"], + "numQubits": params["numQubits"], + }, ) + return Circuit(qsharp_factory=qsharp_factory, encoding="jordan-wigner") - return Circuit( - qsharp_factory=qsharp_factory, - qsharp_op=qsharp_op, - encoding="jordan-wigner", + def _create_isometry(self, params: dict) -> Circuit: + """Create a standalone isometry circuit (binary encoding + GF2+X expansion). + + Args: + params: The parameter dict for Q# circuit construction. + + Returns: + A Circuit containing the binary-encoding operations followed by + the GF2+X expansion operations. + + """ + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingExpansion, + parameter={ + "binaryEncodingOps": params["binaryEncodingOps"], + "gaussianEliminationOps": params["gaussianEliminationOps"], + "numQubits": params["numQubits"], + "ancillaPool": params["ancillaPool"], + }, ) + return Circuit(qsharp_factory=qsharp_factory, encoding="jordan-wigner") def _perform_binary_encoding( self, gf2x_result: GF2XEliminationResult, n_qubits: int diff --git a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs index ec7de8312..aee46bc39 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/BinaryEncoding.qs @@ -13,6 +13,7 @@ namespace QDKChemistry.Utils.BinaryEncoding { import Std.Math.Lg; import Std.Measurement.MResetX; import Std.StatePreparation.PreparePureStateD; + import QDKChemistry.Utils.StatePreparation.ApplyDensePreparation; /// A single gate produced by the matrix compression pipeline. /// @@ -325,20 +326,9 @@ namespace QDKChemistry.Utils.BinaryEncoding { qs : Qubit[], ) : Unit { // Step 1: Dense state prep on reduced subspace - PreparePureStateD(params.stateVector, Subarray(params.rowMap, qs)); - - // Build ancilla pool from indices - let poolQubits = Subarray(params.ancillaPool, qs); - - // Step 2: Apply binary-encoding ops (pre-reversed by Python) - for gate in params.binaryEncodingOps { - ApplyMatrixCompressionOp(gate, qs, poolQubits); - } - - // Step 3: Expand back via GF2+X operations (CX/X only — no pool needed) - for gate in params.gaussianEliminationOps { - ApplyMatrixCompressionOp(gate, qs, []); - } + ApplyDensePreparation(params.rowMap, params.stateVector, qs); + // Step 2 & 3: Apply binary-encoding operations and GF2+X operations + ApplyExpansion(params.binaryEncodingOps, params.gaussianEliminationOps, qs, params.ancillaPool); } /// Create a callable for the binary-encoding state preparation. @@ -379,4 +369,33 @@ namespace QDKChemistry.Utils.BinaryEncoding { ancillaPool = ancillaPool, }, qs); } + + /// Applies the binary-encoding operations followed by GF2+X expansion operations. + operation ApplyExpansion( + binaryEncodingOps : MatrixCompressionOp[], + gaussianEliminationOps : MatrixCompressionOp[], + qs : Qubit[], + ancillaPool : Int[], + ) : Unit { + let poolQubits = Subarray(ancillaPool, qs); + for gate in binaryEncodingOps { + ApplyMatrixCompressionOp(gate, qs, poolQubits); + } + for gate in gaussianEliminationOps { + ApplyMatrixCompressionOp(gate, qs, []); + } + } + + /// Circuit entry point for the isometry stage of binary-encoding + /// state preparation. + /// Allocates qubits and delegates to ApplyExpansion. + operation MakeBinaryEncodingExpansion( + binaryEncodingOps : MatrixCompressionOp[], + gaussianEliminationOps : MatrixCompressionOp[], + numQubits : Int, + ancillaPool : Int[], + ) : Unit { + use qs = Qubit[numQubits]; + ApplyExpansion(binaryEncodingOps, gaussianEliminationOps, qs, ancillaPool); + } } diff --git a/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs b/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs index 687a4f18d..b024c010f 100644 --- a/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs +++ b/python/src/qdk_chemistry/utils/qsharp/src/StatePreparation.qs @@ -36,10 +36,8 @@ namespace QDKChemistry.Utils.StatePreparation { params : StatePreparationParams, qs : Qubit[], ) : Unit { - PreparePureStateD(params.stateVector, Subarray(params.rowMap, qs)); - for gate in params.expansionOps { - ApplyMatrixCompressionOp(gate, qs, []); - } + ApplyDensePreparation(params.rowMap, params.stateVector, qs); + ApplyExpansion(params.expansionOps, qs); } /// A helper function to create a callable for state preparation. @@ -132,4 +130,42 @@ namespace QDKChemistry.Utils.StatePreparation { function MakePrepareSingleReferenceStateOp(params : SingleReferenceParams) : Qubit[] => Unit { PrepareSingleReferenceState(params, _) } + + /// Prepares the dense statevector on the qubit subset given by rowMap. + operation ApplyDensePreparation( + rowMap : Int[], + stateVector : Double[], + qs : Qubit[], + ) : Unit { + PreparePureStateD(stateVector, Subarray(rowMap, qs)); + } + + /// Circuit entry point for the dense state preparation stage. + operation MakeDenseStatePreparation( + rowMap : Int[], + stateVector : Double[], + numQubits : Int, + ) : Unit { + use qs = Qubit[numQubits]; + ApplyDensePreparation(rowMap, stateVector, qs); + } + + /// Applies the GF2+X expansion operations (CX / X gates) to the full register. + operation ApplyExpansion( + expansionOps : MatrixCompressionOp[], + qs : Qubit[], + ) : Unit { + for gate in expansionOps { + ApplyMatrixCompressionOp(gate, qs, []); + } + } + + /// Circuit entry point for the expansion (isometry) stage. + operation MakeExpansion( + expansionOps : MatrixCompressionOp[], + numQubits : Int, + ) : Unit { + use qs = Qubit[numQubits]; + ApplyExpansion(expansionOps, qs); + } } From c0eb7c2df4e30d3d99cd35a63a90074c753fe368 Mon Sep 17 00:00:00 2001 From: yingrongchen Date: Sat, 25 Apr 2026 00:19:29 +0000 Subject: [PATCH 16/21] fix pre-commit Co-authored-by: Copilot --- .../sparse_isometry_binary_encoding.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index 1a83076fe..c3b85dda7 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -97,11 +97,6 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: parameter=params, ) qsharp_op = QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationOp(*params.values()) - Logger.info( - f"Binary encoding produced {len(params['binaryEncodingOps'])} operations " - f"for {n_qubits}-qubit system with {len(bitstrings)} determinants " - f"using {len(params['ancillaPool'])} pre-existing qubits as ancilla pool" - ) return Circuit( qsharp_factory=qsharp_factory, @@ -190,7 +185,14 @@ def _build_binary_encoding_params(self, wavefunction: Wavefunction) -> dict: numQubits=n_qubits, ancillaPool=ancilla_pool, ) - return vars(state_prep_params) + params = vars(state_prep_params) + + Logger.info( + f"Binary encoding produced {len(params['binaryEncodingOps'])} operations " + f"for {n_qubits}-qubit system with {len(bitstrings)} determinants " + f"using {len(params['ancillaPool'])} pre-existing qubits as ancilla pool" + ) + return params def _create_dense(self, params: dict) -> Circuit: """Create a standalone dense state preparation circuit. From 2c0a467a713613ba99b192f265a41ae50e8e2a0b Mon Sep 17 00:00:00 2001 From: yingrongchen Date: Sat, 25 Apr 2026 15:32:18 +0000 Subject: [PATCH 17/21] fix pre-commit Co-authored-by: Copilot --- .../state_preparation/sparse_isometry.py | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index 3071af3a9..95c8726ee 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -124,7 +124,24 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: "alpha and beta orbitals are not supported for state preparation." ) - params = self._build_state_prep_params(wavefunction) + bitstrings, coeffs = self._wavefunction_to_bitstrings_and_coeffs(wavefunction) + # Check for single determinant case after filtering + if len(bitstrings) == 1: + Logger.info("After filtering, only 1 determinant remains, using single reference state preparation") + return self._prepare_single_reference_state(bitstrings[0]) + + n_qubits = len(bitstrings[0]) + Logger.debug(f"Using {len(bitstrings)} determinants for state preparation") + + # Perform GF2+X elimination with tracking + gf2x_operation_results, statevector_data = self._perform_gf2x(bitstrings, coeffs) + Logger.debug(f"gf2x_operation_results dense qubit: {gf2x_operation_results.row_map}") + Logger.debug(f"gf2x_operation_results state vector: {statevector_data}") + + if self._settings.get("dense_preparation_method") == "qiskit": + return self._qiskit_dense_preparation(gf2x_operation_results, statevector_data, n_qubits) + + params = self._build_qsharp_state_prep_params(wavefunction) qsharp_factory = QsharpFactoryData( program=QSHARP_UTILS.StatePreparation.MakeStatePreparationCircuit, @@ -135,17 +152,14 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: state_prep_op = QSHARP_UTILS.StatePreparation.MakeStatePreparationOp(state_prep_params) return Circuit(qsharp_factory=qsharp_factory, qsharp_op=state_prep_op, encoding="jordan-wigner") - def _build_state_prep_params(self, wavefunction: Wavefunction) -> dict: - """Build state preparation parameters from a wavefunction. - - Extracts coefficients and determinants, performs GF2+X elimination, - and returns the parameter dict for Q# circuit construction. + def _wavefunction_to_bitstrings_and_coeffs(self, wavefunction: Wavefunction) -> tuple[list[str], np.ndarray]: + """Extract bitstrings and coefficients from a wavefunction. Args: wavefunction: The target wavefunction to prepare. Returns: - A parameter dict for Q# circuit construction. + A tuple containing a list of bitstrings and a corresponding array of coefficients. """ coeffs = wavefunction.get_coefficients() @@ -156,23 +170,24 @@ def _build_state_prep_params(self, wavefunction: Wavefunction) -> dict: alpha_str, beta_str = det.to_binary_strings(num_orbitals) bitstring = beta_str[::-1] + alpha_str[::-1] # Qiskit uses little-endian convention bitstrings.append(bitstring) + return bitstrings, coeffs - # Check for single determinant case after filtering - if len(bitstrings) == 1: - Logger.info("After filtering, only 1 determinant remains, using single reference state preparation") - return self._prepare_single_reference_state(bitstrings[0]) + def _build_qsharp_state_prep_params(self, wavefunction: Wavefunction) -> dict: + """Build state preparation parameters from a wavefunction. - n_qubits = len(bitstrings[0]) - Logger.debug(f"Using {len(bitstrings)} determinants for state preparation") + Extracts coefficients and determinants, performs GF2+X elimination, + and returns the parameter dict for Q# circuit construction. - # Perform GF2+X elimination with tracking - gf2x_operation_results, statevector_data = self._perform_gf2x(bitstrings, coeffs) - Logger.debug(f"gf2x_operation_results dense qubit: {gf2x_operation_results.row_map}") - Logger.debug(f"gf2x_operation_results state vector: {statevector_data}") + Args: + wavefunction: The target wavefunction to prepare. - if self._settings.get("dense_preparation_method") == "qiskit": - return self._qiskit_dense_preparation(gf2x_operation_results, statevector_data, n_qubits) + Returns: + A parameter dict for Q# circuit construction. + """ + bitstrings, coeffs = self._wavefunction_to_bitstrings_and_coeffs(wavefunction) + n_qubits = len(bitstrings[0]) + gf2x_operation_results, statevector_data = self._perform_gf2x(bitstrings, coeffs) # Use QDK dense state preparation expansion_ops: list[MatrixCompressionOp] = [] for operation in reversed(gf2x_operation_results.operations): From 6626f097ec48d5ba4ca19df24e6c0a61e85bfad2 Mon Sep 17 00:00:00 2001 From: yingrongchen Date: Sat, 25 Apr 2026 17:30:35 +0000 Subject: [PATCH 18/21] improve binary encoding cost Co-authored-by: Copilot --- .../qdk_chemistry/utils/binary_encoding.py | 77 ++++++++++++++++--- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index e9274757e..7c949c090 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -1058,8 +1058,8 @@ def _synthesize_single_pui_lookup_block( rest_entries: Per-target offsets and changing controls. Returns: - ``(ops, select_count)`` where ``select_count`` is the number of - emitted SELECT/SELECT_AND operations, used as a cost proxy. + ``(ops, toffoli_cost)`` where ``toffoli_cost`` is the estimated + number of Toffoli gates used by the emitted SELECT operations. """ if not rest_entries: @@ -1081,11 +1081,13 @@ def _synthesize_single_pui_lookup_block( ) gf2x_ops = list(reversed(lookup_ops)) - select_count = sum( - 1 for name, _ in lookup_ops if name in (MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND) + toffoli_cost = sum( + _select_toffoli_cost(data_table) + for name, (data_table, _, _) in lookup_ops + if name in (MatrixCompressionType.SELECT, MatrixCompressionType.SELECT_AND) ) - return gf2x_ops, select_count + return gf2x_ops, toffoli_cost def _canonicalize_pui_controls( self, @@ -1212,6 +1214,57 @@ def _build_pui_lookup_table( return table +def _is_data_all_zeros(data: list[list[bool]]) -> bool: + """Return True when every row of data is all-false.""" + return all(not any(row) for row in data) + + +def _scs_toffoli_cost(data: list[list[bool]]) -> int: + """Toffoli cost of the ``SparseOneHotSCS`` recursion (singly-controlled). + + Each non-trivial split (N > 1) uses one AND gate (= 1 Toffoli with + measurement-based uncompute). + """ + n = len(data) + if n == 0 or _is_data_all_zeros(data) or n == 1: + return 0 + k = math.ceil(math.log2(n)) + half = 2 ** (k - 1) + left, right = data[:half], data[half:] + left_empty = _is_data_all_zeros(left) + right_empty = _is_data_all_zeros(right) + if not left_empty and not right_empty: + return 1 + _scs_toffoli_cost(left) + _scs_toffoli_cost(right) + if not right_empty: + return 1 + _scs_toffoli_cost(right) + if not left_empty: + return 1 + _scs_toffoli_cost(left) + return 0 + + +def _select_toffoli_cost(data: list[list[bool]]) -> int: + """Estimate the Toffoli count for a ``SparseOneHotSelect`` call. + + The first address-bit split is free (no AND gate); each branch + delegates to ``SparseOneHotSCS``. + """ + n = len(data) + if n == 0 or _is_data_all_zeros(data) or n == 1: + return 0 + k = math.ceil(math.log2(n)) + half = 2 ** (k - 1) + left, right = data[:half], data[half:] + left_empty = _is_data_all_zeros(left) + right_empty = _is_data_all_zeros(right) + if not left_empty and not right_empty: + return _scs_toffoli_cost(left) + _scs_toffoli_cost(right) + if not right_empty: + return _scs_toffoli_cost(right) + if not left_empty: + return _scs_toffoli_cost(left) + return 0 + + def _lookup_select( table_dict: dict[tuple[int, ...], tuple[int, ...]], address_qubits: list[int], @@ -1243,16 +1296,18 @@ def _lookup_select( n_data = len(data_qubits) n_entries = 1 << n_address - # Build dense Bool[][] table from sparse dict. - # addr_tuple uses little-endian bit ordering: addr_tuple[0] = LSB. + # Reverse the address qubit order so that entries that share low-order + # address bits are grouped earlier in the tree. + reversed_address = list(reversed(address_qubits)) + + # Build dense Bool[][] table with reversed bit ordering. data_table: list[list[bool]] = [[False] * n_data for _ in range(n_entries)] for addr_tuple, data_tuple in table_dict.items(): - addr_int = sum(int(bit) << i for i, bit in enumerate(addr_tuple)) + reversed_tuple = tuple(reversed(addr_tuple)) + addr_int = sum(int(bit) << i for i, bit in enumerate(reversed_tuple)) data_table[addr_int] = [bool(b) for b in data_tuple] - # Our table index also uses little-endian (addr_tuple[0] = LSB), so - # pass address_qubits directly without reversing. op_type = MatrixCompressionType.SELECT_AND if use_measurement_and else MatrixCompressionType.SELECT - operations.append((op_type, (data_table, list(address_qubits), list(data_qubits)))) + operations.append((op_type, (data_table, reversed_address, list(data_qubits)))) return operations From 54866ea11ab9a3a25fec930bad17175dbc4bccda Mon Sep 17 00:00:00 2001 From: yingrongchen Date: Mon, 27 Apr 2026 02:54:32 +0000 Subject: [PATCH 19/21] fix test_utils_binary_encoding --- python/tests/test_utils_binary_encoding.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/python/tests/test_utils_binary_encoding.py b/python/tests/test_utils_binary_encoding.py index eae3e79e7..3a5d5bf39 100644 --- a/python/tests/test_utils_binary_encoding.py +++ b/python/tests/test_utils_binary_encoding.py @@ -714,20 +714,22 @@ def test_two_address_bits(self): assert len(ops) == 1 name, (data_table, addr, dat) = ops[0] assert name == MatrixCompressionType.SELECT - assert addr == [0, 1] + assert addr == [1, 0] assert dat == [2] assert len(data_table) == 4 def test_data_table_correctness(self): """Verify the dense Bool[][] table encodes the sparse dict correctly.""" # Address (1,0) → data (1,0), address (0,1) → data (0,1) + # After reversal: (1,0) → reversed (0,1) → addr_int=2 + # (0,1) → reversed (1,0) → addr_int=1 table = {(1, 0): (1, 0), (0, 1): (0, 1)} ops = _lookup_select(table, [0, 1], [2, 3]) _, (data_table, _, _) = ops[0] - # addr_int for (1,0): bit0=1, bit1=0 → addr_int=1 - assert data_table[1] == [True, False] - # addr_int for (0,1): bit0=0, bit1=1 → addr_int=2 - assert data_table[2] == [False, True] + # addr_int for (1,0): reversed to (0,1), bit0=0, bit1=1 → addr_int=2 + assert data_table[2] == [True, False] + # addr_int for (0,1): reversed to (1,0), bit0=1, bit1=0 → addr_int=1 + assert data_table[1] == [False, True] # Other entries should be all-false assert data_table[0] == [False, False] assert data_table[3] == [False, False] From ac9b40c67b7c1de15aa2a2b4ea5062911be92e97 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Tue, 28 Apr 2026 23:07:25 +0000 Subject: [PATCH 20/21] fallback to prepare from gf2x matrix if already dense --- .../sparse_isometry_binary_encoding.py | 131 +++++++++++++----- .../qdk_chemistry/utils/binary_encoding.py | 13 +- .../test_state_preparation_binary_encoding.py | 84 ++++++++++- python/tests/test_utils_binary_encoding.py | 39 ++++++ 4 files changed, 228 insertions(+), 39 deletions(-) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index c3b85dda7..cff4dd3f8 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -18,7 +18,12 @@ from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.data.circuit import QsharpFactoryData from qdk_chemistry.utils import Logger -from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer, MatrixCompressionOp, MatrixCompressionType +from qdk_chemistry.utils.binary_encoding import ( + BinaryEncodingSynthesizer, + MatrixCompressionOp, + MatrixCompressionType, + _dense_qubits_size, +) from qdk_chemistry.utils.qsharp import QSHARP_UTILS from .sparse_isometry import ( @@ -90,7 +95,37 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: """ Logger.trace_entering() - params = self._build_binary_encoding_params(wavefunction) + coeffs = wavefunction.get_coefficients() + dets = wavefunction.get_active_determinants() + num_orbitals = len(wavefunction.get_orbitals().get_active_space_indices()[0]) + bitstrings = [] + for det in dets: + alpha_str, beta_str = det.to_binary_strings(num_orbitals) + bitstrings.append(beta_str[::-1] + alpha_str[::-1]) + + if len(bitstrings) == 1: + Logger.info("After filtering, only 1 determinant remains, using single reference state preparation") + return self._prepare_single_reference_state(bitstrings[0]) + + n_qubits = len(bitstrings[0]) + Logger.debug(f"Using {len(bitstrings)} determinants for state preparation") + + # GF2+X elimination — skip the diagonal reduction because binary encoding's + # stage-1 handles the identity pivot block natively; the extra CX + X ops + # from diagonal reduction would be redundant. + bitstring_matrix = self._bitstrings_to_binary_matrix(bitstrings) + gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, forward_only=True) + + # Check applicability: binary encoding needs at least one spare row beyond + # the dense register for the one-hot batch indicators. + num_rows, num_cols = gf2x_result.reduced_matrix.shape + if _dense_qubits_size(num_cols) >= num_rows: + Logger.info( + "Binary encoding is not applicable for this wavefunction; falling back to dense+GF2X state preparation." + ) + return self._build_gf2x_circuit(gf2x_result, coeffs, n_qubits) + + params = self._build_binary_encoding_params(gf2x_result, coeffs, n_qubits, bitstrings) qsharp_factory = QsharpFactoryData( program=QSHARP_UTILS.BinaryEncoding.MakeBinaryEncodingStatePreparationCircuit, @@ -104,42 +139,26 @@ def _run_impl(self, wavefunction: Wavefunction) -> Circuit: encoding="jordan-wigner", ) - def _build_binary_encoding_params(self, wavefunction: Wavefunction) -> dict: - """Build binary-encoding state preparation parameters from a wavefunction. - - Extracts coefficients and determinants, performs GF2+X elimination and - binary-encoding synthesis, and returns the parameter dict for Q# circuit - construction. + def _build_binary_encoding_params( + self, + gf2x_result: GF2XEliminationResult, + coeffs: np.ndarray, + n_qubits: int, + bitstrings: list[str], + ) -> dict: + """Build binary-encoding state preparation parameters from an already-computed REF result. Args: - wavefunction: The target wavefunction to prepare. + gf2x_result: Forward-only REF result from GF2+X elimination. + coeffs: Wavefunction coefficients aligned with matrix columns. + n_qubits: Total number of qubits in the original space. + bitstrings: Determinant bitstrings (used for logging only). Returns: - A dict of parameters for Q# circuit construction. + A dict of parameters for Q# binary-encoding circuit construction. """ - coeffs = wavefunction.get_coefficients() - dets = wavefunction.get_active_determinants() - num_orbitals = len(wavefunction.get_orbitals().get_active_space_indices()[0]) - bitstrings = [] - for det in dets: - alpha_str, beta_str = det.to_binary_strings(num_orbitals) - bitstrings.append(beta_str[::-1] + alpha_str[::-1]) - - if len(bitstrings) == 1: - Logger.info("After filtering, only 1 determinant remains, using single reference state preparation") - return self._prepare_single_reference_state(bitstrings[0]) - - n_qubits = len(bitstrings[0]) - Logger.debug(f"Using {len(bitstrings)} determinants for state preparation") - - # Step 1: GF2+X elimination — skip the diagonal reduction because - # binary encoding's stage-1 handles the identity pivot block natively; - # the extra CX + X ops from diagonal reduction would be redundant. - bitstring_matrix = self._bitstrings_to_binary_matrix(bitstrings) - gf2x_result = gf2x_with_tracking(bitstring_matrix, skip_diagonal_reduction=True, forward_only=True) - - # Step 2: Binary encoding on the REF matrix + # Step 1: Binary encoding on the REF matrix encoded_ops, bijection, dense_size = self._perform_binary_encoding(gf2x_result, n_qubits) # Step 2b: Build compressed statevector reindexed by the bijection. @@ -194,6 +213,54 @@ def _build_binary_encoding_params(self, wavefunction: Wavefunction) -> dict: ) return params + def _build_gf2x_circuit(self, gf2x_result: GF2XEliminationResult, coeffs: np.ndarray, n_qubits: int) -> Circuit: + """Build a dense+GF2X state preparation circuit from a REF result. + + Args: + gf2x_result: Forward-only REF result from GF2+X elimination. + coeffs: Wavefunction coefficients aligned with matrix columns. + n_qubits: Total number of qubits in the original space. + + Returns: + A Circuit using dense state preparation followed by GF2+X expansion. + + """ + # Build statevector: each column of the reduced matrix maps to a unique + # computational-basis state via little-endian binary encoding. + statevector = np.zeros(2**gf2x_result.rank, dtype=float) + for det_idx in range(gf2x_result.reduced_matrix.shape[1]): + reduced_col = gf2x_result.reduced_matrix[:, det_idx] + sv_index = int("".join(str(b) for b in reversed(reduced_col)), 2) + statevector[sv_index] = coeffs[det_idx] + norm = np.linalg.norm(statevector) + if norm > 0: + statevector /= norm + + # Build expansion ops (reversed GF2+X ops). + expansion_ops: list[MatrixCompressionOp] = [] + for op in reversed(gf2x_result.operations): + if op[0] in ("cx", "cnot") and isinstance(op[1], tuple): + target, control = op[1] + expansion_ops.append(MatrixCompressionOp(MatrixCompressionType.CX, [control, target])) + elif op[0] == "x" and isinstance(op[1], int): + expansion_ops.append(MatrixCompressionOp(MatrixCompressionType.X, [op[1]])) + + # row_map[::-1] matches the parent's Q# PreparePureStateD convention (row rank-1 = MSB). + state_prep_params = QSHARP_UTILS.StatePreparation.StatePreparationParams( + rowMap=gf2x_result.row_map[::-1], + stateVector=statevector.tolist(), + expansionOps=[op.to_dict() for op in expansion_ops], + numQubits=n_qubits, + ) + params = vars(state_prep_params) + + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.StatePreparation.MakeStatePreparationCircuit, + parameter=params, + ) + state_prep_op = QSHARP_UTILS.StatePreparation.MakeStatePreparationOp(state_prep_params) + return Circuit(qsharp_factory=qsharp_factory, qsharp_op=state_prep_op, encoding="jordan-wigner") + def _create_dense(self, params: dict) -> Circuit: """Create a standalone dense state preparation circuit. diff --git a/python/src/qdk_chemistry/utils/binary_encoding.py b/python/src/qdk_chemistry/utils/binary_encoding.py index 7c949c090..4faa91353 100644 --- a/python/src/qdk_chemistry/utils/binary_encoding.py +++ b/python/src/qdk_chemistry/utils/binary_encoding.py @@ -173,8 +173,7 @@ def __init__(self, data: np.ndarray): Raises: NotRefError: If ``data`` is not in REF. - AssertionError: If the matrix rank/size assumptions required by - the algorithm are violated. + ValueError: If ``data`` is not a 2-dimensional array. """ self.data = np.asarray(data, dtype=np.int8) @@ -185,10 +184,6 @@ def __init__(self, data: np.ndarray): self.num_rows, self.num_cols = self.data.shape self.dense_size = _dense_qubits_size(self.num_cols) - if self.dense_size >= self.num_rows: - raise ValueError( - f"Dense size ({self.dense_size}) must be strictly less than number of rows ({self.num_rows})" - ) self._tmp_row = np.zeros(self.num_cols, dtype=np.int8) self.pivots = self.identify_pivots() @@ -374,6 +369,12 @@ def __init__( """ self.tableau = tableau + if tableau.dense_size >= tableau.num_rows: + raise ValueError( + f"Binary encoding is not applicable: state is already dense " + f"({tableau.num_cols} determinant(s) require a {tableau.dense_size}-qubit dense register, " + f"leaving no spare rows in a {tableau.num_rows}-row matrix)." + ) self.include_negative_controls = include_negative_controls self.measurement_based_uncompute = measurement_based_uncompute diff --git a/python/tests/test_state_preparation_binary_encoding.py b/python/tests/test_state_preparation_binary_encoding.py index 507cc637d..ac6651baa 100644 --- a/python/tests/test_state_preparation_binary_encoding.py +++ b/python/tests/test_state_preparation_binary_encoding.py @@ -14,7 +14,7 @@ from qdk_chemistry.algorithms.state_preparation.sparse_isometry import gf2x_with_tracking from qdk_chemistry.data import Circuit, Wavefunction from qdk_chemistry.plugins.qiskit import QDK_CHEMISTRY_HAS_QISKIT -from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer, MatrixCompressionType +from qdk_chemistry.utils.binary_encoding import BinaryEncodingSynthesizer, MatrixCompressionType, _dense_qubits_size from .reference_tolerances import float_comparison_absolute_tolerance, float_comparison_relative_tolerance from .test_helpers import create_random_wavefunction @@ -207,3 +207,85 @@ def test_random_wavefunction_statevector(self, n_electrons, n_orbitals, n_dets, assert np.isclose( overlap, 1.0, atol=float_comparison_absolute_tolerance, rtol=float_comparison_relative_tolerance ) + + @pytest.mark.parametrize( + ("n_electrons", "n_orbitals", "n_dets", "seed", "expected_n_qubits"), + [ + # 4 electrons, 3 orbitals, 9 determinants: the full space has only + # ceil(6 choose 4) = 15 states. After GF2+X (forward-only, no + # diagonal reduction) the REF matrix has rank 4 (4 rows) but still + # 9 columns, so dense_size = _dense_qubits_size(9) = 4 = num_rows. + # The condition dense_size >= num_rows triggers the fallback. + (4, 3, 9, 0, 6), + (4, 3, 9, 1, 6), + ], + ids=["4e3o_9det_seed0", "4e3o_9det_seed1"], + ) + def test_fallback_to_dense_gf2x(self, n_electrons, n_orbitals, n_dets, seed, expected_n_qubits): + """Wavefunction where after GF2+X the REF matrix is already dense falls back to dense+GF2X.""" + wf = create_random_wavefunction( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=n_dets, + seed=seed, + ) + + # Confirm this case is genuinely a fallback case before testing. + num_orbitals = len(wf.get_orbitals().get_active_space_indices()[0]) + bitstrings = [] + for det in wf.get_active_determinants(): + a, b = det.to_binary_strings(num_orbitals) + bitstrings.append(b[::-1] + a[::-1]) + mat = np.array([[int(c) for c in bs] for bs in bitstrings], dtype=np.int8).T + gf2x_result = gf2x_with_tracking(mat, skip_diagonal_reduction=True, forward_only=True) + num_rows, num_cols = gf2x_result.reduced_matrix.shape + assert _dense_qubits_size(num_cols) >= num_rows, ( + f"Expected fallback: dense_size={_dense_qubits_size(num_cols)} must be >= num_rows={num_rows}" + ) + + circuit = create("state_prep", "sparse_isometry_binary_encoding").run(wf) + assert isinstance(circuit, Circuit) + assert circuit.encoding == "jordan-wigner" + + lc = circuit.estimate()["logicalCounts"] + # No binary-encoding SELECT/SELECT_AND ops in the fallback path. + assert lc["cczCount"] == 0 + # System qubits only — PreparePureStateD does not need external ancilla. + assert lc["numQubits"] == expected_n_qubits + + @pytest.mark.skipif(not QDK_CHEMISTRY_HAS_QISKIT, reason="Qiskit not available") + @pytest.mark.parametrize( + ("n_electrons", "n_orbitals", "n_dets", "seed"), + [ + (4, 3, 9, 0), + (4, 3, 9, 1), + ], + ids=["4e3o_9det_seed0", "4e3o_9det_seed1"], + ) + def test_fallback_statevector(self, n_electrons, n_orbitals, n_dets, seed): + """Fallback circuit produces the correct statevector (Qiskit simulation). + + Validates that the fallback dense+GF2X path correctly encodes the target + wavefunction amplitudes, not merely that it runs without error. + """ + from qiskit.quantum_info import Statevector # noqa: PLC0415 + + from qdk_chemistry.plugins.qiskit.conversion import create_statevector_from_wavefunction # noqa: PLC0415 + + wf = create_random_wavefunction( + n_electrons=n_electrons, + n_orbitals=n_orbitals, + n_dets=n_dets, + seed=seed, + ) + circuit = create("state_prep", "sparse_isometry_binary_encoding").run(wf) + expected_sv = create_statevector_from_wavefunction(wf, normalize=True) + n_system = 2 * n_orbitals + + qc = circuit.get_qiskit_circuit() + sim_data = np.array(Statevector.from_instruction(qc)) + system_sv = sim_data[: 2**n_system] + overlap = np.abs(np.vdot(expected_sv, system_sv)) + assert np.isclose( + overlap, 1.0, atol=float_comparison_absolute_tolerance, rtol=float_comparison_relative_tolerance + ) diff --git a/python/tests/test_utils_binary_encoding.py b/python/tests/test_utils_binary_encoding.py index 3a5d5bf39..a5e370d61 100644 --- a/python/tests/test_utils_binary_encoding.py +++ b/python/tests/test_utils_binary_encoding.py @@ -249,6 +249,45 @@ def test_from_matrix_rejects_non_ref(self): with pytest.raises(NotRefError): BinaryEncodingSynthesizer.from_matrix(mat) + @pytest.mark.parametrize( + ("matrix", "expected_num_cols", "expected_dense_size", "expected_num_rows"), + [ + # 4 columns need a 2-qubit dense register, but there are only 2 rows — no spare row. + ( + np.array([[1, 0, 0, 0], [0, 1, 0, 0]], dtype=np.int8), + 4, + 2, + 2, + ), + # 2 columns need a 1-qubit dense register, but there is only 1 row — no spare row. + ( + np.array([[1, 0]], dtype=np.int8), + 2, + 1, + 1, + ), + # 8 columns need a 3-qubit dense register, but there are only 3 rows — no spare row. + ( + np.array( + [[1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0]], + dtype=np.int8, + ), + 8, + 3, + 3, + ), + ], + ) + def test_rejects_already_dense_tableau(self, matrix, expected_num_cols, expected_dense_size, expected_num_rows): + """Already-dense tableau must raise ValueError with an informative message.""" + with pytest.raises(ValueError, match="Binary encoding is not applicable") as exc_info: + BinaryEncodingSynthesizer(RefTableau(matrix)) + + msg = str(exc_info.value) + assert f"{expected_num_cols} determinant(s)" in msg + assert f"{expected_dense_size}-qubit dense register" in msg + assert f"{expected_num_rows}-row matrix" in msg + def test_max_batch_size_power_of_two(self): """max_batch_size must return a positive power of two.""" mat = np.array( From 32363468cfc12f8ce0224c01884fefd0b68aa751 Mon Sep 17 00:00:00 2001 From: Rushi Gong Date: Thu, 21 May 2026 18:25:12 +0000 Subject: [PATCH 21/21] rename gf2x sparse isometry --- .../source/_static/examples/python/circuit.py | 2 +- .../examples/python/phase_estimation.py | 2 +- .../_static/examples/python/quickstart.py | 2 +- .../examples/python/release_notes_v1_1.py | 2 +- .../examples/python/state_preparation.py | 4 +-- .../algorithms/state_preparation.rst | 2 +- examples/extended_hubbard.ipynb | 2 +- examples/factory_list.ipynb | 8 +++--- .../qiskit/iqpe_no_trotter.py | 2 +- .../interoperability/qiskit/iqpe_trotter.py | 2 +- examples/qpe_stretched_n2.ipynb | 2 +- examples/state_prep_energy.ipynb | 4 +-- .../src/qdk_chemistry/algorithms/registry.py | 4 +-- .../algorithms/state_preparation/__init__.py | 4 +-- .../state_preparation/sparse_isometry.py | 18 ++++++------- .../sparse_isometry_binary_encoding.py | 8 +++--- .../state_preparation/state_preparation.py | 4 +-- python/tests/test_encoding_metadata.py | 8 +++--- python/tests/test_energy_estimator.py | 4 +-- ...rop_qiskit_state_prep_energy_validation.py | 22 ++++++++-------- python/tests/test_state_preparation.py | 26 +++++++++---------- 21 files changed, 66 insertions(+), 66 deletions(-) diff --git a/docs/source/_static/examples/python/circuit.py b/docs/source/_static/examples/python/circuit.py index a2685f5f5..e44282321 100644 --- a/docs/source/_static/examples/python/circuit.py +++ b/docs/source/_static/examples/python/circuit.py @@ -104,7 +104,7 @@ _, wfn_cas = create("multi_configuration_calculator").run(ham, 1, 1) # StatePreparation produces a Circuit with a native Q# factory -state_prep = create("state_prep", "sparse_isometry_gf2x") +state_prep = create("state_prep", "sparse_isometry") circuit = state_prep.run(wfn_cas) # Inspect the Q# circuit (prune unused qubits for clarity) diff --git a/docs/source/_static/examples/python/phase_estimation.py b/docs/source/_static/examples/python/phase_estimation.py index ce44385de..6ee9034ee 100644 --- a/docs/source/_static/examples/python/phase_estimation.py +++ b/docs/source/_static/examples/python/phase_estimation.py @@ -66,7 +66,7 @@ qubit_ham = qubit_mapper.run(hamiltonian) # 6. State preparation -state_prep = create("state_prep", "sparse_isometry_gf2x") +state_prep = create("state_prep", "sparse_isometry") circuit = state_prep.run(wfn_cas) # 7. Create and run IQPE with nested algorithm settings diff --git a/docs/source/_static/examples/python/quickstart.py b/docs/source/_static/examples/python/quickstart.py index 2956c0c63..128ca7cf7 100644 --- a/docs/source/_static/examples/python/quickstart.py +++ b/docs/source/_static/examples/python/quickstart.py @@ -94,7 +94,7 @@ # start-state-prep-circuit # Generate state preparation circuit for the sparse state via sparse isometry (GF2 + X) -state_prep = create("state_prep", "sparse_isometry_gf2x") +state_prep = create("state_prep", "sparse_isometry") sparse_isometry_circuit = state_prep.run(wfn_sparse) # end-state-prep-circuit diff --git a/docs/source/_static/examples/python/release_notes_v1_1.py b/docs/source/_static/examples/python/release_notes_v1_1.py index db68ce4ed..5c1a40396 100644 --- a/docs/source/_static/examples/python/release_notes_v1_1.py +++ b/docs/source/_static/examples/python/release_notes_v1_1.py @@ -199,7 +199,7 @@ _cas = create("multi_configuration_calculator", "macis_cas") _E_cas, _wfn_cas = _cas.run(_hamiltonian, 1, 1) -_state_prep = create("state_prep", "sparse_isometry_gf2x") +_state_prep = create("state_prep", "sparse_isometry") _circuit = _state_prep.run(_wfn_cas) circuit_executor = create("circuit_executor", "qdk_sparse_state_simulator") diff --git a/docs/source/_static/examples/python/state_preparation.py b/docs/source/_static/examples/python/state_preparation.py index e70b5ebca..34e11edab 100644 --- a/docs/source/_static/examples/python/state_preparation.py +++ b/docs/source/_static/examples/python/state_preparation.py @@ -10,7 +10,7 @@ from qdk_chemistry.algorithms import create # Create a StatePreparation instance -sparse_prep = create("state_prep", "sparse_isometry_gf2x") +sparse_prep = create("state_prep", "sparse_isometry") regular_prep = create("state_prep", "qiskit_regular_isometry") # end-cell-create ################################################################################ @@ -61,6 +61,6 @@ from qdk_chemistry.algorithms import registry print(registry.available("state_prep")) -# ['sparse_isometry_gf2x', 'qiskit_regular_isometry'] +# ['sparse_isometry', 'qiskit_regular_isometry'] # end-cell-list-implementations ################################################################################ diff --git a/docs/source/user/comprehensive/algorithms/state_preparation.rst b/docs/source/user/comprehensive/algorithms/state_preparation.rst index a0a212785..76346dc9a 100644 --- a/docs/source/user/comprehensive/algorithms/state_preparation.rst +++ b/docs/source/user/comprehensive/algorithms/state_preparation.rst @@ -78,7 +78,7 @@ You can discover available implementations programmatically: Sparse Isometry GF2+X ~~~~~~~~~~~~~~~~~~~~~ -.. rubric:: Factory name: ``"sparse_isometry_gf2x"`` +.. rubric:: Factory name: ``"sparse_isometry"`` This method is an optimized approach that leverages sparsity in the target wavefunction. The GF2+X method, a modification of the original sparse isometry work in :cite:`Malvetti2021`, applies GF(2) Gaussian elimination to the binary matrix representation of the state to determine a reduced space representation of the sparse state. This reduced state is then densely encoded via regular isometry :cite:`Christandl2016` on a smaller number of qubits, and finally scattered to the full qubit space using X and :term:`CNOT` gates. These reductions correspond to efficient gate sequences that simplify the preparation basis. By focusing only on non-zero amplitudes, this approach substantially reduces circuit depth and gate count compared with dense isometry methods. This method is native to QDK/Chemistry and is especially efficient for wavefunctions with sparse amplitude structure. diff --git a/examples/extended_hubbard.ipynb b/examples/extended_hubbard.ipynb index b0c952cde..efc020249 100644 --- a/examples/extended_hubbard.ipynb +++ b/examples/extended_hubbard.ipynb @@ -502,7 +502,7 @@ "from qdk.widgets import Circuit\n", "\n", "# Generate state preparation circuit for the sparse state via GF2+X sparse isometry\n", - "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "state_prep = create(\"state_prep\", \"sparse_isometry\")\n", "state_prep_circuit = state_prep.run(wfn_trial)\n", "\n", "# Visualize the sparse isometry circuit\n", diff --git a/examples/factory_list.ipynb b/examples/factory_list.ipynb index d9f05b7b5..c7501f355 100644 --- a/examples/factory_list.ipynb +++ b/examples/factory_list.ipynb @@ -1134,28 +1134,28 @@ "\n", "\n", "state_prep\n", - "sparse_isometry_gf2x\n", + "sparse_isometry\n", "State preparation using sparse isometry with enhanced GF2+X elimination.\n", "basis_gates\n", "['x', 'y', 'z', 'cx', 'cz', 'id', 'h', 's', 'sdg', 'rz']\n", "\n", "\n", "state_prep\n", - "sparse_isometry_gf2x\n", + "sparse_isometry\n", "State preparation using sparse isometry with enhanced GF2+X elimination.\n", "dense_preparation_method\n", "qdk\n", "\n", "\n", "state_prep\n", - "sparse_isometry_gf2x\n", + "sparse_isometry\n", "State preparation using sparse isometry with enhanced GF2+X elimination.\n", "transpile\n", "True\n", "\n", "\n", "state_prep\n", - "sparse_isometry_gf2x\n", + "sparse_isometry\n", "State preparation using sparse isometry with enhanced GF2+X elimination.\n", "transpile_optimization_level\n", "0\n", diff --git a/examples/interoperability/qiskit/iqpe_no_trotter.py b/examples/interoperability/qiskit/iqpe_no_trotter.py index c35d60d06..98ec70f23 100644 --- a/examples/interoperability/qiskit/iqpe_no_trotter.py +++ b/examples/interoperability/qiskit/iqpe_no_trotter.py @@ -228,7 +228,7 @@ def run_iterative_exact_qpe( active_hamiltonian, list(top_configurations.keys()) ) -sparse_state_prep = create("state_prep", algorithm_name="sparse_isometry_gf2x") +sparse_state_prep = create("state_prep", algorithm_name="sparse_isometry") state_prep = sparse_state_prep.run(sparse_wavefunction).get_qiskit_circuit() state_prep = transpile( state_prep, diff --git a/examples/interoperability/qiskit/iqpe_trotter.py b/examples/interoperability/qiskit/iqpe_trotter.py index 68ea5f8a7..9ef3ba490 100644 --- a/examples/interoperability/qiskit/iqpe_trotter.py +++ b/examples/interoperability/qiskit/iqpe_trotter.py @@ -94,7 +94,7 @@ active_hamiltonian, list(top_configurations.keys()) ) -sparse_state_prep = create("state_prep", algorithm_name="sparse_isometry_gf2x") +sparse_state_prep = create("state_prep", algorithm_name="sparse_isometry") state_prep = sparse_state_prep.run(sparse_wavefunction).get_qiskit_circuit() state_prep = transpile( state_prep, diff --git a/examples/qpe_stretched_n2.ipynb b/examples/qpe_stretched_n2.ipynb index e7fc4ed32..53fc7fc93 100644 --- a/examples/qpe_stretched_n2.ipynb +++ b/examples/qpe_stretched_n2.ipynb @@ -359,7 +359,7 @@ "from qdk.widgets import Circuit\n", "\n", "# Generate state preparation circuit for the sparse state via GF2+X sparse isometry\n", - "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "state_prep = create(\"state_prep\", \"sparse_isometry\")\n", "sparse_isometry_circuit = state_prep.run(wfn_trial)\n", "\n", "# Visualize the sparse isometry circuit, idle and classical qubits are removed\n", diff --git a/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index d28568391..fe6bc41ba 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -343,7 +343,7 @@ "from qdk.widgets import Circuit\n", "\n", "# Generate state preparation circuit for the sparse state via sparse isometry (GF2 + X)\n", - "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "state_prep = create(\"state_prep\", \"sparse_isometry\")\n", "sparse_isometry_circuit = state_prep.run(wfn_sparse)\n", "\n", "# Visualize the sparse isometry circuit\n", @@ -460,4 +460,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/python/src/qdk_chemistry/algorithms/registry.py b/python/src/qdk_chemistry/algorithms/registry.py index e264e5d5d..006872cf6 100644 --- a/python/src/qdk_chemistry/algorithms/registry.py +++ b/python/src/qdk_chemistry/algorithms/registry.py @@ -590,7 +590,7 @@ def _register_python_algorithms(): ) from qdk_chemistry.algorithms.qubit_hamiltonian_solver import DenseMatrixSolver, SparseMatrixSolver # noqa: PLC0415 from qdk_chemistry.algorithms.qubit_mapper import QdkQubitMapper # noqa: PLC0415 - from qdk_chemistry.algorithms.state_preparation import SparseIsometryGF2XStatePreparation # noqa: PLC0415 + from qdk_chemistry.algorithms.state_preparation import SparseIsometryStatePreparation # noqa: PLC0415 from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import ( # noqa: PLC0415 SparseIsometryBinaryEncodingStatePreparation, ) @@ -601,7 +601,7 @@ def _register_python_algorithms(): ) register(lambda: QdkEnergyEstimator()) - register(lambda: SparseIsometryGF2XStatePreparation()) + register(lambda: SparseIsometryStatePreparation()) register(lambda: SparseIsometryBinaryEncodingStatePreparation()) register(lambda: DenseMatrixSolver()) register(lambda: SparseMatrixSolver()) diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py b/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py index 05a24d03c..59d59af08 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/__init__.py @@ -9,7 +9,7 @@ # -------------------------------------------------------------------------------------------- from qdk_chemistry.algorithms.state_preparation.sparse_isometry import ( - SparseIsometryGF2XStatePreparation, + SparseIsometryStatePreparation, ) from qdk_chemistry.algorithms.state_preparation.sparse_isometry_binary_encoding import ( SparseIsometryBinaryEncodingStatePreparation, @@ -22,7 +22,7 @@ __all__ = [ "SparseIsometryBinaryEncodingStatePreparation", - "SparseIsometryGF2XStatePreparation", + "SparseIsometryStatePreparation", "StatePreparationFactory", "StatePreparationSettings", ] diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py index 95c8726ee..b94356e4b 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry.py @@ -6,7 +6,7 @@ prepare only the non-zero amplitude components, significantly reducing circuit depth and gate count compared to dense state preparation methods. -**SparseIsometryGF2XStatePrep**: Enhanced sparse isometry using GF2+X elimination. +**SparseIsometryStatePrep**: Enhanced sparse isometry using GF2+X elimination. This method performs duplicate row removal, all-ones row removal, and diagonal matrix rank reduction besides standard GF2 Gaussian elimination. It tracks both CNOT and X operations for optimal circuit reconstruction and can be more @@ -22,7 +22,7 @@ Algorithm Details: -* SparseIsometryGF2X: Applies enhanced GF2+X elimination (preprocessing + GF2 +* SparseIsometry: Applies enhanced GF2+X elimination (preprocessing + GF2 + postprocessing), performs dense state preparation on the reduced space, then applies recorded operations (CX and X) in reverse to expand back to the full space. @@ -45,11 +45,11 @@ from qdk_chemistry.utils.binary_encoding import MatrixCompressionOp, MatrixCompressionType from qdk_chemistry.utils.qsharp import QSHARP_UTILS -__all__: list[str] = ["SparseIsometryGF2XStatePreparationSettings"] +__all__: list[str] = ["SparseIsometryStatePreparationSettings"] -class SparseIsometryGF2XStatePreparationSettings(StatePreparationSettings): - """Settings for SparseIsometryGF2XStatePreparation.""" +class SparseIsometryStatePreparationSettings(StatePreparationSettings): + """Settings for SparseIsometryStatePreparation.""" def __init__(self): """Initialize the StatePreparationSettings.""" @@ -59,7 +59,7 @@ def __init__(self): ) -class SparseIsometryGF2XStatePreparation(StatePreparation): +class SparseIsometryStatePreparation(StatePreparation): """State preparation using sparse isometry with enhanced GF2+X elimination. This class implements "GF2+X" state preparation for electronic structure problems using @@ -90,10 +90,10 @@ class SparseIsometryGF2XStatePreparation(StatePreparation): """ def __init__(self) -> None: - """Initialize the SparseIsometryGF2XStatePreparation.""" + """Initialize the SparseIsometryStatePreparation.""" Logger.trace_entering() super().__init__() - self._settings = SparseIsometryGF2XStatePreparationSettings() + self._settings = SparseIsometryStatePreparationSettings() def _run_impl(self, wavefunction: Wavefunction) -> Circuit: """Prepare a quantum circuit that encodes the given wavefunction using sparse isometry over GF(2^x). @@ -492,7 +492,7 @@ def _prepare_single_reference_state(self, bitstring: str) -> Circuit: def name(self) -> str: """Return the name of the state preparation method.""" Logger.trace_entering() - return "sparse_isometry_gf2x" + return "sparse_isometry" @dataclass diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py index cff4dd3f8..05e1128d4 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/sparse_isometry_binary_encoding.py @@ -28,8 +28,8 @@ from .sparse_isometry import ( GF2XEliminationResult, - SparseIsometryGF2XStatePreparation, - SparseIsometryGF2XStatePreparationSettings, + SparseIsometryStatePreparation, + SparseIsometryStatePreparationSettings, gf2x_with_tracking, ) @@ -38,7 +38,7 @@ ] -class SparseIsometryBinaryEncodingSettings(SparseIsometryGF2XStatePreparationSettings): +class SparseIsometryBinaryEncodingSettings(SparseIsometryStatePreparationSettings): """Settings for SparseIsometryBinaryEncodingStatePreparation.""" def __init__(self): @@ -58,7 +58,7 @@ def __init__(self): ) -class SparseIsometryBinaryEncodingStatePreparation(SparseIsometryGF2XStatePreparation): +class SparseIsometryBinaryEncodingStatePreparation(SparseIsometryStatePreparation): """State preparation using sparse isometry with binary encoding. This class extends sparse isometry with GF2+X elimination by replacing diff --git a/python/src/qdk_chemistry/algorithms/state_preparation/state_preparation.py b/python/src/qdk_chemistry/algorithms/state_preparation/state_preparation.py index 43ae01935..5c80b457d 100644 --- a/python/src/qdk_chemistry/algorithms/state_preparation/state_preparation.py +++ b/python/src/qdk_chemistry/algorithms/state_preparation/state_preparation.py @@ -76,5 +76,5 @@ def algorithm_type_name(self) -> str: return "state_prep" def default_algorithm_name(self) -> str: - """Return the sparse_isometry_gf2x as default algorithm name.""" - return "sparse_isometry_gf2x" + """Return the sparse_isometry as default algorithm name.""" + return "sparse_isometry" diff --git a/python/tests/test_encoding_metadata.py b/python/tests/test_encoding_metadata.py index 486e7082c..105e56030 100644 --- a/python/tests/test_encoding_metadata.py +++ b/python/tests/test_encoding_metadata.py @@ -181,8 +181,8 @@ def test_validate_encoding_compatibility_mismatch(): def test_state_preparation_injects_jordan_wigner_encoding(wavefunction_4e4o): """Test that StatePreparation algorithms inject Jordan-Wigner encoding.""" - # Test sparse_isometry_gf2x - prep_gf2x = create("state_prep", "sparse_isometry_gf2x") + # Test sparse_isometry + prep_gf2x = create("state_prep", "sparse_isometry") circuit_gf2x = prep_gf2x.run(wavefunction_4e4o) assert circuit_gf2x.encoding == "jordan-wigner" @@ -250,7 +250,7 @@ def test_end_to_end_workflow_compatible_encodings(wavefunction_4e4o): qubit_ham = mapper.run(hamiltonian) # Create Circuit with state preparation (should be Jordan-Wigner) - prep = create("state_prep", "sparse_isometry_gf2x") + prep = create("state_prep", "sparse_isometry") circuit = prep.run(wavefunction_4e4o) # Validation should pass @@ -266,7 +266,7 @@ def test_end_to_end_workflow_incompatible_encodings(wavefunction_4e4o): qubit_ham = mapper.run(hamiltonian) # Create Circuit with state preparation (should be Jordan-Wigner) - prep = create("state_prep", "sparse_isometry_gf2x") + prep = create("state_prep", "sparse_isometry") circuit = prep.run(wavefunction_4e4o) # Validation should fail diff --git a/python/tests/test_energy_estimator.py b/python/tests/test_energy_estimator.py index e5cab6ab4..99dd31c84 100644 --- a/python/tests/test_energy_estimator.py +++ b/python/tests/test_energy_estimator.py @@ -85,7 +85,7 @@ def test_append_measurement_to_circuit_qasm(basis, n_qubits, expect_measure, exp def test_create_measurement_circuits_basic(wavefunction_4e4o): """Test measurement circuit generation for a simple observable.""" - state_prep = create("state_prep", "sparse_isometry_gf2x") + state_prep = create("state_prep", "sparse_isometry") circuit = state_prep.run(wavefunction_4e4o) # Define observable @@ -281,7 +281,7 @@ def test_estimator_run_4e4o(executor_name, wavefunction_4e4o, ref_energy_4e4o): The energy offset and test Hamiltonian are derived from classical wavefunction information, which is used to pre-screen the qubit Hamiltonian and identify terms requiring quantum measurement. """ - state_prep = create("state_prep", "sparse_isometry_gf2x") + state_prep = create("state_prep", "sparse_isometry") state_prep_circuit = state_prep.run(wavefunction_4e4o) energy_offset = -4.19142869944708 test_hamiltonian = QubitHamiltonian( diff --git a/python/tests/test_interop_qiskit_state_prep_energy_validation.py b/python/tests/test_interop_qiskit_state_prep_energy_validation.py index 97d8a19af..2873f7dd8 100644 --- a/python/tests/test_interop_qiskit_state_prep_energy_validation.py +++ b/python/tests/test_interop_qiskit_state_prep_energy_validation.py @@ -11,7 +11,7 @@ import pytest from qdk_chemistry.algorithms import create -from qdk_chemistry.algorithms.state_preparation.sparse_isometry import SparseIsometryGF2XStatePreparation +from qdk_chemistry.algorithms.state_preparation.sparse_isometry import SparseIsometryStatePreparation from qdk_chemistry.data import Circuit from qdk_chemistry.plugins.qiskit import QDK_CHEMISTRY_HAS_QISKIT, QDK_CHEMISTRY_HAS_QISKIT_AER @@ -39,7 +39,7 @@ def test_energy_agreement_between_state_prep_methods(wavefunction_4e4o, hamilton # Create both state preparation instances basis_gates = ["cx", "rz", "ry", "rx", "h", "x", "z"] sparse_prep_gf2x = create( - "state_prep", algorithm_name="sparse_isometry_gf2x", transpile_optimization_level=1, basis_gates=basis_gates + "state_prep", algorithm_name="sparse_isometry", transpile_optimization_level=1, basis_gates=basis_gates ) regular_prep = create( "state_prep", algorithm_name="qiskit_regular_isometry", transpile_optimization_level=1, basis_gates=basis_gates @@ -71,19 +71,19 @@ def test_energy_agreement_between_state_prep_methods(wavefunction_4e4o, hamilton ), f"Energy difference {energy_diff} exceeds tolerance. " -def test_sparse_isometry_gf2x_energy_validation(wavefunction_10e6o, hamiltonian_10e6o, ref_energy_10e6o): - """Test SparseIsometryGF2XStatePreparation energy validation for 10e6o F2.""" - # Create SparseIsometryGF2XStatePreparation instance for F2 test +def test_sparse_isometry_energy_validation(wavefunction_10e6o, hamiltonian_10e6o, ref_energy_10e6o): + """Test SparseIsometryStatePreparation energy validation for 10e6o F2.""" + # Create SparseIsometryStatePreparation instance for F2 test sparse_prep = create( "state_prep", - algorithm_name="sparse_isometry_gf2x", + algorithm_name="sparse_isometry", basis_gates=["cx", "rz", "ry", "rx", "h", "x", "z"], transpile_optimization_level=1, ) qiskit_sparse_prep = create( "state_prep", - algorithm_name="sparse_isometry_gf2x", + algorithm_name="sparse_isometry", basis_gates=["cx", "rz", "ry", "rx", "h", "x", "z"], transpile_optimization_level=1, dense_preparation_method="qiskit", @@ -122,16 +122,16 @@ def test_sparse_isometry_gf2x_energy_validation(wavefunction_10e6o, hamiltonian_ ) -def test_sparse_isometry_gf2x_circuit_efficiency(wavefunction_4e4o): +def test_sparse_isometry_circuit_efficiency(wavefunction_4e4o): """Compare isometry resource requirements. - Test that SparseIsometryGF2XStatePrep creates more circuits using fewer resources + Test that SparseIsometryStatePrep creates more circuits using fewer resources than regular isometry. """ # Create both state preparation instances basis_gates = ["cx", "rz", "ry", "rx", "h", "x", "z"] sparse_prep = create( - "state_prep", algorithm_name="sparse_isometry_gf2x", transpile_optimization_level=1, basis_gates=basis_gates + "state_prep", algorithm_name="sparse_isometry", transpile_optimization_level=1, basis_gates=basis_gates ) regular_prep = create( "state_prep", algorithm_name="qiskit_regular_isometry", transpile_optimization_level=1, basis_gates=basis_gates @@ -188,7 +188,7 @@ def get_bitstring(circuit: Circuit) -> str: @pytest.mark.parametrize("bitstring", ["1010", "0000", "1111", "101001", "1", "0"]) def test_single_reference_state_basic(bitstring): """Test basic single reference state preparation with various bitstrings.""" - test_cls = SparseIsometryGF2XStatePreparation() + test_cls = SparseIsometryStatePreparation() circuit = test_cls._prepare_single_reference_state(bitstring) result_bitstring = get_bitstring(circuit) assert result_bitstring == bitstring, f"Expected {bitstring}, got {result_bitstring}" diff --git a/python/tests/test_state_preparation.py b/python/tests/test_state_preparation.py index b06d5cc6c..b4aa23b1f 100644 --- a/python/tests/test_state_preparation.py +++ b/python/tests/test_state_preparation.py @@ -23,7 +23,7 @@ from qdk_chemistry.algorithms import create from qdk_chemistry.algorithms.state_preparation.sparse_isometry import ( GF2XEliminationResult, - SparseIsometryGF2XStatePreparation, + SparseIsometryStatePreparation, _eliminate_column, _find_pivot_row, _is_diagonal_matrix, @@ -59,9 +59,9 @@ def test_regular_isometry_state_prep(wavefunction_4e4o): assert int(qubit_pattern.group(1)) == 2 * 4 -def test_sparse_isometry_gf2x_basic(wavefunction_4e4o): +def test_sparse_isometry_basic(wavefunction_4e4o): """Test the sparse isometry GF(2^X) StatePreparation algorithm basic functionality.""" - prep = create("state_prep", "sparse_isometry_gf2x") + prep = create("state_prep", "sparse_isometry") # Test circuit creation circuit = prep.run(wavefunction_4e4o) assert isinstance(circuit, Circuit) @@ -79,9 +79,9 @@ def test_sparse_isometry_gf2x_basic(wavefunction_4e4o): @pytest.mark.skipif(not QDK_CHEMISTRY_HAS_QISKIT, reason="Qiskit not available") -def test_sparse_isometry_gf2x_qiskit_dense_prepare(wavefunction_4e4o): +def test_sparse_isometry_qiskit_dense_prepare(wavefunction_4e4o): """Test the sparse isometry GF(2^X) StatePreparation algorithm basic functionality.""" - prep = create("state_prep", "sparse_isometry_gf2x", dense_preparation_method="qiskit") + prep = create("state_prep", "sparse_isometry", dense_preparation_method="qiskit") # Test circuit creation circuit = prep.run(wavefunction_4e4o) assert isinstance(circuit, Circuit) @@ -96,8 +96,8 @@ def test_sparse_isometry_gf2x_qiskit_dense_prepare(wavefunction_4e4o): assert f"{expected_theta:.6f}" in qasm # expected angle -def test_sparse_isometry_gf2x_single_reference_state(): - """Test SparseIsometryGF2XStatePrep with single reference state after filtering.""" +def test_sparse_isometry_single_reference_state(): + """Test SparseIsometryStatePrep with single reference state after filtering.""" # Create a wavefunction with coefficients that will be filtered out test_orbitals = create_test_orbitals(2) @@ -108,7 +108,7 @@ def test_sparse_isometry_gf2x_single_reference_state(): container = CasWavefunctionContainer(coeffs, dets, test_orbitals) wavefunction = Wavefunction(container) - prep = create("state_prep", "sparse_isometry_gf2x") + prep = create("state_prep", "sparse_isometry") single_ref_circuit = prep.run(wavefunction) assert isinstance(single_ref_circuit, Circuit) @@ -136,7 +136,7 @@ def test_sparse_isometry_gf2x_single_reference_state(): def test_gf2x_bitstrings_to_binary_matrix(): """Test functionality of _bitstrings_to_binary_matrix helper.""" - testclass = SparseIsometryGF2XStatePreparation() + testclass = SparseIsometryStatePreparation() # Simple 3-qubit, 2-determinant example bitstrings = ["101", "010"] # q[2]q[1]q[0] format (Little Endian) result = testclass._bitstrings_to_binary_matrix(bitstrings) @@ -221,7 +221,7 @@ def test_gf2x_bitstrings_to_binary_matrix(): def test_gf2x_bitstrings_to_binary_matrix_edge_cases(): """Test edge cases and error conditions for bitstring-to-matrix conversion.""" - testclass = SparseIsometryGF2XStatePreparation() + testclass = SparseIsometryStatePreparation() # Empty bitstrings list with pytest.raises(ValueError, match="Bitstrings list cannot be empty"): @@ -258,7 +258,7 @@ def test_gf2x_bitstrings_to_binary_matrix_edge_cases(): def test_gf2x_bitstrings_to_binary_matrix_qiskit_convention(): """Test that the function correctly handles Qiskit Little Endian convention.""" - testclass = SparseIsometryGF2XStatePreparation() + testclass = SparseIsometryStatePreparation() # Test specific example bitstrings = ["101", "010"] # q[2]q[1]q[0] format @@ -290,7 +290,7 @@ def test_gf2x_bitstrings_to_binary_matrix_qiskit_convention(): def test_gf2x_bitstrings_to_binary_matrix_additional_validation(): """Test additional validation scenarios for bitstrings_to_binary_matrix.""" - testclass = SparseIsometryGF2XStatePreparation() + testclass = SparseIsometryStatePreparation() # Test with large valid bitstring large_bitstring = ["0" * 50, "1" * 50] @@ -302,7 +302,7 @@ def test_gf2x_bitstrings_to_binary_matrix_additional_validation(): def test_prepare_single_reference_state_error_cases(): """Test error handling for invalid inputs.""" - test_cls = SparseIsometryGF2XStatePreparation() + test_cls = SparseIsometryStatePreparation() with pytest.raises(ValueError, match="Bitstring cannot be empty"): test_cls._prepare_single_reference_state("")