As a first pass (because I'm about to leave on vacation), I put together a plan with Claude and then let Claude run.
Merge requests:
Initial Prompt
Please develop a plan to add the following capabilities to the analysis tool:
- Compute the eigendecomposition of the unitary matrix equivalent to the algorithm.
- Compute the exact matrix form for the algorithm (that is, no Trotter or double-factorization, but the exact version).
- Compute the eigendecomposition of the exact matrix.
- Perform numerical simulation using the exact matrix.
- Compute the eigenvalue error: the difference between the eigenvalue from the exact matrix and the eigenvalue from the unitary matrix equivalent to the algorithm. This will probably compare the lowest eigenvalues.
- Compute the matrix-norm error: the norm of the difference between the unitary equivalent of the algorithm and the exact matrix.
- Compute the state-dependent error: compare the final state from numerical simulation using the unitary equivalent to the algorithm and the final state from numerical simulation using the exact matrix.
Use the branch bkk_numerical as a starting point on which to build new changes.
The analyses discussed here have significant overlap in the things they need to compute. As with the bkk_numerical branch, any shared results (especially intermediate results from expensive calculations) should be computed once and then re-used as appropriate. Where appropriate, memory should be cleaned up once intermediate results that use significant memory are no longer needed.
These tasks should be appropriately distributed across multiple git merge requests for "bite size" reviews. Branches should either be independent or should build on each other as appropriate, but any dependencies between branches/merge requests needs to be clearly indicated.
All appropriate testing should be included (unit and integration). All appropriate documentation (README files, code comments, etc) updates should be made. Intermediate documentation explaining your process should not be added to the repository. Examples should be expanded and/or new examples should be added -- see analysis/examples/config2.py for an example of an example configuration file that is intended to exercise all analyses. The config2.py file may need to be updated to reflect all current analyses and also renamed before beginning the above tasks.
All tests (unit and integration) should be added to both the GitHub and GitLab CI pipelines. All tests should be run for each deliverable (that is, each branch that will be used for a merge request).
I will likely be unable to work through all these tasks before leaving on travel, so save the plan and the current status (at all reasonable break points, such as completing a branch, etc) to a file so that it can be picked up and continued later.
Claude's Plan
Context
The qhat analysis tool currently supports:
- Computing unitary matrices from quantum algorithms (with Trotter/double-factorization approximations)
- Performing numerical simulations using these unitary matrices
- Resource estimation
This plan adds capabilities to compute exact Hamiltonian matrices (without Trotter or approximation), their eigendecompositions, and various error metrics comparing the approximate algorithm to the exact solution. These features are critical for validating algorithm accuracy and understanding the impact of approximations.
The work builds on the bkk_numerical branch, which introduced:
- Unitary matrix computation via
_compute_unitary_matrix()
- Numerical simulation with
numerical_simulation()
- Shared intermediate result pattern (compute once, reuse)
- Analysis framework in
analysis/analysis.py
Recent main branch updates (merged after bkk_numerical diverged):
- Major logging refactor: Centralized logging architecture with
common/logging_utils.py
- All modules now use standard
logging.getLogger(__name__) pattern
- Removed
mock_config.py from tests in favor of conftest.py with logging fixtures
config_types.py: Removed inline logging configuration (now handled by logging_utils)
analysis.py: All functions now use module-level logger instead of config_general.log_*()
- These changes will need to be merged into
bkk_numerical before proceeding with implementation
Architecture Overview
Current Structure
- analysis/analysis.py: Main analysis dispatcher with
analyze_algorithm()
- analysis/config_types.py: Configuration classes (AnalysisConfiguration, etc.)
- analysis/hamiltonian.py: Hamiltonian class with Pauli string generation
- analysis/unitary.py: Unitary encoding methods (Trotter, double-factorization)
- analysis/file_io.py: Matrix and state I/O utilities
- Shared results pattern: Expensive computations (like unitary matrix) computed once and passed to multiple analyses
Required Additions
- Exact matrix computation: Convert Hamiltonian Pauli strings directly to matrix form
- Eigendecomposition: Compute eigenvalues/eigenvectors for both exact and unitary matrices
- Error metrics: Three types of errors comparing exact vs approximate
- Configuration: Extend AnalysisConfiguration to request new analyses
- Testing: Unit tests for matrix operations, integration tests for full pipeline
- CI/CD: Add new tests to GitHub Actions and GitLab CI
Implementation Plan: Branch Structure
IMPORTANT: Merge Strategy
Before starting Branch 0, the bkk_numerical branch must be updated with the latest changes from main. The main branch has undergone a significant logging refactor that affects:
- How modules obtain loggers (now
logging.getLogger(__name__))
- Test infrastructure (removed
mock_config.py, added conftest.py)
- Configuration classes (removed inline logging setup)
Recommended approach:
- Merge
main into bkk_numerical: git merge main
- Resolve conflicts in files that both branches modified:
analysis/analysis.py: bkk_numerical added numerical simulation; main refactored logging
analysis/config_types.py: bkk_numerical added numerical_simulation_inputs; main refactored logging
analysis/file_io.py: bkk_numerical added state I/O; main refactored logging
analysis/hamiltonian.py: Both branches have minor changes
analysis/tests/: bkk_numerical added new tests; main added conftest.py and removed mock_config.py
- Update
bkk_numerical code to use new logging patterns:
- Replace
config_general.log() with logger.info()
- Replace
config_general.log_verbose() with logger.verbose()
- Replace
config_general.log_debug() with logger.debug()
- Update test files to use
conftest.py instead of mock_config.py
- Run all tests to verify compatibility:
python3.11 -m pytest analysis/tests/ -v
- Then proceed with Branch 0
Expected conflicts:
- All logging calls in
analysis.py functions (numerical_simulation, output_unitary_matrix)
- Test imports in
test_numerical_simulation.py (references mock_config.py which was removed)
- Minor formatting differences in other files
Files unique to bkk_numerical (no conflicts, just keep them):
analysis/tests/test_numerical_simulation.py
analysis/tests/test_analysis_integration.py
- Enhanced
analysis/README.md sections
Alternative approach (if merge is complex):
- Rebase
bkk_numerical onto current main: git rebase main
- This gives a cleaner history but requires more careful conflict resolution
- Same logging pattern updates will be needed
The work is organized into 5 sequential branches (each builds on the updated bkk_numerical):
Branch 0: bkk_config_full_analysis (Baseline Configuration)
Purpose: Establish comprehensive example configuration that exercises all current QHAT analyses
Builds on: bkk_numerical
Files to modify:
- Rename
analysis/examples/config2.py → analysis/examples/config_full_analysis.py
- Update the file to include ALL currently available analyses:
- Resource estimation (pyLIQTR)
- Matrix output (unitary matrix from algorithm)
- Numerical simulation
- Update
analysis/README.md to reference config_full_analysis.py instead of config2.py
- Update any other documentation that references
config2.py
Key implementation details:
- This branch adds NO new features - just improves the example
- The configuration should demonstrate:
- All resource estimation options
- Matrix output to different formats (npz/h5/txt)
- Numerical simulation with multiple input states
- Comments explaining what each analysis does and when to use it
- Acts as baseline: subsequent branches will add new analyses to this example
Testing:
- Run
config_full_analysis.py and verify all existing analyses work
- Verify output files are created as expected
- No new tests needed (existing tests cover functionality)
Merge criteria:
- Example runs successfully on test system
- README updated
- All references to config2.py updated
- CI/CD passes (no functional changes, just rename/reorganization)
Branch 1: bkk_exact_matrix (Foundation)
Purpose: Compute exact Hamiltonian matrices without approximations
Builds on: bkk_config_full_analysis
Files to modify:
analysis/analysis.py: Add _compute_exact_matrix() helper and exact_matrix_output() analysis
analysis/config_types.py: Add exact_matrix_output_file field to AnalysisConfiguration
analysis/hamiltonian.py: Add to_matrix() method to Hamiltonian class
analysis/README.md: Document new exact matrix capability
analysis/examples/config_full_analysis.py: Add example of exact matrix output
.github/workflows/python-app.yml: Ensure tests run
.gitlab-ci.yml: Ensure tests run
New files:
analysis/matrix_operations.py: Utility functions for Pauli string operations
pauli_char_to_matrix(): Convert 'I', 'X', 'Y', 'Z' to 2x2 matrices (for small systems)
pauli_string_to_matrix(): Build full matrix from string (for small systems)
pauli_dict_to_matrix(): Sum weighted Pauli strings into dense matrix (for small systems)
PauliStringOperator class: Matrix-free linear operator for large systems
- Stores Hamiltonian as Pauli strings (sparse representation)
- Implements
matvec(v) to apply operator to vector without materializing matrix
- Compatible with scipy sparse eigensolvers
- Scales to 25-30+ qubits (limited by state vector size, not matrix size)
analysis/tests/test_matrix_operations.py: Unit tests for matrix utilities
analysis/tests/test_exact_matrix.py: Integration tests for exact matrix computation
Key implementation details:
-
Reuse hamiltonian.get_all_pauli_strings(return_as='strings') to get coefficient dict
-
Two-path design based on system size:
Small systems (≤15 qubits): Dense path
- Materialize full dense matrix using existing
pauli_dict_to_matrix()
- Store as numpy array
- Fast, simple, reusable
Large systems (>15 qubits): Matrix-free path
- Create
PauliStringOperator that stores Pauli strings
- Implements
matvec(v) for matrix-vector products without materializing matrix
- Compatible with scipy
LinearOperator interface
- Memory: O(num_pauli_terms + 2^N) instead of O(2^(2N))
- Can reach 25-30+ qubits
-
Automatic selection: Check dimension, use dense if ≤32768 (15 qubits), else matrix-free
-
User override: Optional force_dense or force_sparse parameters
-
Matrix output to file:
- Dense: save directly via
save_matrix()
- Matrix-free: Either refuse (dimension too large) or save Pauli string representation
-
Key insight: Different analyses may need different representations - some cache, some compute on-the-fly
Testing:
- Unit: Test Pauli conversion functions with known matrices
- Unit: Test matrix properties (Hermiticity, eigenvalue bounds match 1-norm)
- Integration: Test full pipeline with small Hamiltonian (2-3 qubits)
- Integration: Verify exact matrix matches expected form for test cases
Merge criteria:
- All unit and integration tests pass
- GitHub Actions and GitLab CI green
- README updated with exact matrix documentation
- Example config demonstrates usage
Branch 2: bkk_eigendecomposition (Eigenanalysis)
Purpose: Compute eigendecompositions of exact and unitary matrices
Builds on: bkk_exact_matrix
Files to modify:
analysis/analysis.py: Add eigendecomposition_analysis() function
analysis/config_types.py: Add eigendecomposition config fields to AnalysisConfiguration:
num_eigenvalues: int or str (number of eigenvalues to compute, default=0)
- 0 = disabled (no eigendecomposition)
- positive int = compute that many eigenvalues (e.g., 5 for ground state + 4 excited states)
- "all" = compute full eigendecomposition (all eigenvalues and eigenvectors)
eigendecomposition_matrices: str ('exact', 'approximate', or 'both', default='approximate')
- 'approximate' = only eigendecompose the algorithm's unitary (default - avoids extra computation)
- 'exact' = only eigendecompose the exact Hamiltonian matrix
- 'both' = eigendecompose both (useful for comparison, but computes both matrices)
which_eigenvalues: str ('smallest', 'largest', or 'both', default='smallest')
- 'smallest' = algebraically smallest (most negative, ground state for Hamiltonians)
- 'largest' = algebraically largest (most positive)
- NOT based on magnitude/absolute value
- Ignored when num_eigenvalues = "all" (full decomposition includes all)
analysis/file_io.py: Add save_eigendecomposition() and load_eigendecomposition()
analysis/README.md: Document eigendecomposition analysis
analysis/examples/config_full_analysis.py: Add eigendecomposition configuration
New files:
analysis/tests/test_eigendecomposition.py: Tests for eigendecomposition functionality
Key implementation details:
- Eigendecomposition triggered when
num_eigenvalues != 0 (either positive int or "all")
- User controls which matrices to compute/decompose via
eigendecomposition_matrices:
'approximate' (default): Only compute approximate matrix and its eigendecomposition → approximate_eigendecomposition.npz
'exact': Only compute exact matrix and its eigendecomposition → exact_eigendecomposition.npz
'both': Compute both matrices and both eigendecompositions → two output files
Matrix representation strategy:
- For eigendecomposition, we don't always need to materialize the full matrix
scipy.sparse.linalg.eigsh() accepts either:
- A matrix (dense or sparse)
- A
LinearOperator with matvec(v) method
- Use matrix-free operator for large systems (dimension > 32768)
- Pass
PauliStringOperator or approximate algorithm's operator directly to eigsh
- Computes eigenvalues without ever materializing the full matrix
- Enables 25-30 qubit eigendecomposition
Eigendecomposition modes:
-
Full eigendecomposition (num_eigenvalues = "all"):
- Use
scipy.linalg.eigh() to compute all eigenvalues/eigenvectors
- Requires dense matrix - automatically refuse if dimension > 32768
- Suitable for systems ≤15 qubits only
which_eigenvalues parameter ignored (all eigenvalues computed anyway)
- Clear error message: "Full eigendecomposition not supported for >15 qubits, use num_eigenvalues=k"
-
Partial eigendecomposition (num_eigenvalues > 0):
- Use sparse methods:
scipy.sparse.linalg.eigsh() with k parameter
- Works with matrix-free operators - scales to 25-30+ qubits
- Respects
which_eigenvalues setting
- Can compute k << dimension efficiently even for very large matrices
- This is the primary production path
-
Implementation: Check if isinstance(num_eigenvalues, str) and num_eigenvalues.lower() == "all"
-
Memory management: eigsh only stores k eigenvectors (not full basis)
-
Output format for each npz file:
eigenvalues: 1D array of eigenvalues
eigenvectors: 2D array (column i is eigenvector for eigenvalue i)
matrix_type: 'exact' or 'approximate'
num_eigenvalues: number computed
which_eigenvalues: 'smallest', 'largest', or 'both'
- Metadata: git_hash, timestamp
-
Implementation note: Map user-friendly values to scipy's which parameter:
- 'smallest' → 'SA' (Smallest Algebraic - most negative eigenvalues)
- 'largest' → 'LA' (Largest Algebraic - most positive eigenvalues)
- 'both' → compute smallest and largest separately, concatenate results
-
Documentation must emphasize: "smallest" means algebraically smallest (closest to -∞), not smallest magnitude
-
Memory management: Store only requested number of eigenvalues/vectors
Testing:
- Unit: Test eigendecomposition on small known matrices (identity, Pauli matrices)
- Unit: Verify orthonormality of eigenvectors
- Unit: Verify eigenvalue ordering
- Unit: Test
num_eigenvalues = "all" (full decomposition) on small matrix
- Unit: Test
num_eigenvalues = 5 (partial decomposition) on larger matrix
- Unit: Test case insensitivity ("all", "All", "ALL" all work)
- Integration: Test
eigendecomposition_matrices='exact' (one output file)
- Integration: Test
eigendecomposition_matrices='approximate' (one output file)
- Integration: Test
eigendecomposition_matrices='both' (two output files)
- Integration: Test 'smallest', 'largest', and 'both' for
which_eigenvalues
- Integration: Verify full vs partial decomposition give same results (for overlap)
- Integration: Verify no unnecessary matrix computations
Branch 3: bkk_error_metrics (Error Analysis)
Purpose: Compute three error metrics comparing exact vs approximate
Builds on: bkk_eigendecomposition
Files to modify:
analysis/analysis.py: Add error_analysis() function
analysis/config_types.py: Add error analysis fields to AnalysisConfiguration:
error_num_eigenvalues: int (how many eigenvalues to compare; 0 = skip eigenvalue errors, default=0)
error_matrix_norms: str or list of str (which matrix norms to compute; None = skip matrix errors, default=None)
- Supported values: "frobenius", "spectral"
- Can be single string: "frobenius"
- Can be list: ["frobenius", "spectral"]
error_state_inputs: str or list of str (state files for error comparison; None = skip state errors, default=None)
analysis/README.md: Document error analyses
analysis/examples/config_full_analysis.py: Add error analysis configuration
New files:
analysis/tests/test_error_analysis.py: Tests for error metrics
Key implementation details:
Error analysis has three independent components, each explicitly enabled:
-
Eigenvalue Error (enabled when error_num_eigenvalues > 0):
- Requires both exact and approximate eigendecompositions (will compute if not already done)
- Compare smallest k eigenvalues from exact vs approximate
- Output:
eigenvalue_errors array with differences exact_eigs[i] - approx_eigs[i]
- Report both relative and absolute errors
- Typical use:
error_num_eigenvalues = 1 for ground state energy comparison
-
Matrix Norm Error (enabled when error_matrix_norms is not None):
- Computes specified norm(s) of the difference matrix
||U_exact - U_approximate||
- Critical scaling challenge: Computing norms requires comparing the two full matrices
- Two implementation paths:
Small systems (≤15 qubits): Direct computation
- Materialize both dense matrices
- Compute difference:
diff = exact - approximate
- Compute norms directly
Large systems (>15 qubits): Matrix-free computation (expensive but feasible)
Implementation approach for Branch 3:
-
Small systems (≤15 qubits): Direct computation with dense matrices (fast)
-
Large systems (>15 qubits): Matrix-free computation with progress warnings
- Before starting: Log estimated time/cost
- During computation: Show progress updates
- Warning message: "Computing Frobenius norm for 20 qubits requires ~1M matrix operations, estimated time: X minutes"
-
User can always Ctrl+C if it's too slow
-
Suggest state-dependent errors as faster alternative in documentation
-
Frobenius norm ("frobenius"): ||A||_F = sqrt(sum of squares of all elements)
-
Spectral norm ("spectral"): ||A||_2 = largest singular value
-
Normalize input to list internally (handle both string and list inputs)
-
State-Dependent Error (enabled when error_state_inputs is not None):
- Input: string or list of strings specifying initial state files (e.g., "state.npy" or ["state1.npy", "state2.npy"])
- This is the best-scaling error metric for large systems
- For each input state:
- Apply approximate operator:
approx_final = approx_operator @ initial_state
- Apply exact operator:
exact_final = exact_operator @ initial_state
- Compute error:
||exact_final - approx_final||_2
- Report both absolute and relative to state norm
- Key scaling advantage:
- State vectors are O(2^N) not O(2^(2N)) like matrices
- Can reach 30 qubits (16 GB per state vector)
- Applies operators without materializing matrices (matrix-free)
- Implementation:
- For small systems: use materialized matrices if available (fast)
- For large systems: use matrix-free operators (PauliStringOperator)
- Automatically select based on dimension
- This is independent of
numerical_simulation_inputs and exact_simulation_inputs
- Pattern matches numerical simulation: provide filenames to enable the analysis
Output format (npz or JSON file, e.g., error_analysis.npz):
eigenvalue_errors: Array of eigenvalue differences (if eigenvalue error enabled)
eigenvalue_errors_relative: Relative errors (if eigenvalue error enabled)
matrix_frobenius_error: Scalar (if "frobenius" in error_matrix_norms)
matrix_spectral_error: Scalar (if "spectral" in error_matrix_norms)
state_errors: List of dicts with {input_file, absolute_error, relative_error} (if state error enabled)
Memory management:
- Free exact_matrix after computations if no longer needed
- Free unitary_matrix after computations if no longer needed
- Check at end of
analyze_algorithm() and explicitly delete large arrays
Testing:
- Unit: Test error calculations with known matrices
- Unit: Test error = 0 when exact = approximate
- Unit: Test error increases with perturbations
- Unit: Test Frobenius norm calculation (both dense and matrix-free paths)
- Unit: Test spectral norm calculation (both dense and matrix-free paths)
- Unit: Test
error_matrix_norms as string ("frobenius") and as list (["frobenius", "spectral"])
- Unit: Verify warning messages displayed for large systems
- Integration: Test each error type independently (eigenvalue only, matrix only, state only)
- Integration: Test all three error types together
- Integration: Test matrix norms on system just above dense threshold (16 qubits) - verify matrix-free path works
- Integration: Test error when required data missing (should fail with clear message)
- Integration: Verify errors NOT computed when parameters are None/0
- Performance: Document computation time for matrix norms at 15, 16, 18, 20 qubits
Branch 4: bkk_exact_simulation (Complete Pipeline)
Purpose: Numerical simulation using exact matrix
Builds on: bkk_error_metrics
Files to modify:
analysis/analysis.py: Add exact_numerical_simulation() function
analysis/config_types.py: Add to AnalysisConfiguration:
exact_simulation_inputs: string or list (like numerical_simulation_inputs)
analysis/README.md: Document exact simulation
analysis/examples/config_full_analysis.py: Update to demonstrate all analyses together
Key implementation details:
- Very similar to existing
numerical_simulation() function
- Uses exact operator (matrix or matrix-free) instead of unitary
- Output files:
input_exact_final.npy (distinguish from input_final.npy)
- Can be used independently OR in conjunction with approximate simulation for comparison
- Scales to 25-30 qubits using matrix-free operator
- For large systems:
- Don't materialize full matrix
- Apply Hamiltonian via Pauli string evaluation: for each Pauli term, apply to state
- Memory: state vector only (O(2^N)), not matrix (O(2^(2N)))
- Same approach used in state-dependent error analysis
Testing:
- Unit: Test exact simulation with identity matrix
- Unit: Test with known Pauli matrix
- Integration: Compare exact simulation with known analytical result
- Integration: Test multiple input states
- End-to-end: Run full analysis with all features enabled
Cross-Cutting Concerns
Memory Management Pattern
Key architectural change for scale: Don't always materialize full matrices. Use operators (dense matrix or matrix-free) depending on system size.
def analyze_algorithm(...):
# Determine system dimension
num_qubits = hamiltonian.num_qubits()
dimension = 2 ** num_qubits
use_dense = (dimension <= 32768) # 15 qubits threshold
# Track what we need
needs_approximate = (
matrix_output_file or
numerical_simulation_inputs or
error_matrix_norms or
error_state_inputs or
(num_eigenvalues > 0 and eigendecomposition_matrices in ['approximate', 'both'])
)
needs_exact = (
exact_matrix_output_file or
exact_simulation_inputs or
error_matrix_norms or
error_state_inputs or
(num_eigenvalues > 0 and eigendecomposition_matrices in ['exact', 'both'])
)
# Create operators (not always full matrices!)
if needs_approximate:
if use_dense:
approximate_op = _compute_unitary_matrix(...) # Dense numpy array
else:
approximate_op = algorithm # Algorithm bloq with matvec (matrix-free)
if needs_exact:
if use_dense:
exact_op = _compute_exact_matrix_dense(...) # Dense numpy array
else:
exact_op = _create_pauli_operator(hamiltonian) # PauliStringOperator (matrix-free)
# Use operators for analyses (analyses adapt to dense vs matrix-free)
if matrix_output_file:
results["matrix_output"] = output_unitary_matrix(..., approximate_op, use_dense)
if exact_matrix_output_file:
results["exact_matrix_output"] = output_exact_matrix(..., exact_op, use_dense)
if num_eigenvalues > 0:
results["eigendecomposition"] = eigendecomposition_analysis(..., exact_op, approximate_op)
if error_state_inputs:
results["state_errors"] = compute_state_errors(..., exact_op, approximate_op)
if error_matrix_norms:
results["matrix_errors"] = compute_matrix_errors(..., exact_op, approximate_op, use_dense)
# Clean up (only needed for materialized matrices)
if use_dense:
del approximate_op, exact_op
Design principle: Analyses accept operators that may be dense matrices OR LinearOperators with matvec(). Each analysis adapts its implementation based on operator type.
Configuration Validation
- Each analysis function validates its specific requirements
analyze_algorithm() validates at least one analysis is requested
- Provide clear error messages for missing dependencies (e.g., error analysis requires both exact and unitary)
CI/CD Updates
Both .github/workflows/python-app.yml and .gitlab-ci.yml:
- Already have
run-analysis-tests jobs that run pytest
- New tests automatically included via
pytest tests/
- No structural changes needed
- Verify each branch passes before merge
Documentation Strategy
README.md sections to add/update:
- "Matrix Output" section: Add exact matrix subsection
- New "Eigendecomposition Analysis" section
- Explain
num_eigenvalues options:
- Small positive int (e.g., 5): for large systems, compute only a few eigenvalues (RECOMMENDED for production)
- "all": compute full eigendecomposition (all eigenvalues/eigenvectors) for small systems only
- 0: disabled
- Example usage:
analysis.num_eigenvalues = "all" or analysis.num_eigenvalues = 5
- Must clearly explain: 'smallest' = algebraically smallest (most negative, ground state energy)
- Example: For Hamiltonian with eigenvalues [-10, -5, 0, 5, 10], 'smallest' gives [-10, -5, ...]
- Clarify this is NOT smallest magnitude (which would give [0, 5, -5, ...])
- System size guidance:
- ≤10 qubits: "all" is feasible
- 10-12 qubits: "all" possible but expensive
- ≥12 qubits: use partial (specify small k)
- 20 qubits (production scale): MUST use partial, "all" not feasible
- Partial eigendecomposition scales well beyond 20 qubits
- New "Error Analysis" section with subsections for each error type
- Clearly document "frobenius" vs "spectral" norms:
- Frobenius: element-wise difference, fast to compute
- Spectral: worst-case effect on any quantum state, more physically meaningful but expensive
- Include when to use each (Frobenius for quick comparisons, spectral for physical bounds)
- New "Exact Numerical Simulation" section
- Update "Configuration Options" summary
Code comments:
- Docstrings for all new functions
- Inline comments for non-obvious algorithms (e.g., sparse matrix construction)
- NO intermediate process documentation in repo
Example updates:
config_full_analysis.py (renamed in Branch 0) evolves across branches to demonstrate cumulative features
- Each branch adds its new analysis capabilities to this example
- Final version demonstrates complete analysis pipeline with all available features
Testing Strategy
Unit tests:
- Each new module gets comprehensive unit tests
- Test edge cases: empty matrices, single-qubit, identity, etc.
- Test error handling: dimension mismatches, missing data
Integration tests:
- Each branch adds integration test for its new capability
- Integration tests use small systems (2-4 qubits) for CI/CD speed
- At least one test exercises full pipeline end-to-end
Production scale expectations:
- Normal production use: 20 qubits (dimension 2^20 = 1,048,576)
- Target: 25-30 qubits (dimension up to 2^30 = 1 billion)
- Stretch goal: Push toward 50 qubits where feasible
- Higher is always better - users will push to limits
Scaling limits by operation type:
- Dense matrix storage: Limited to ~15 qubits (16 GB at 15 qubits, 16 TB at 20 qubits)
- State vectors: Can reach ~30 qubits (16 GB for 30-qubit state)
- Sparse/matrix-free operations: Can reach 25-30+ qubits depending on Hamiltonian sparsity
Design principles for maximum scale:
- Avoid materializing full dense matrices for large systems
- Matrix-free operators: Compute matrix-vector products directly from Pauli strings
- Sparse eigensolvers: Use iterative methods that only need matrix-vector products
- User control: Let user choose dense vs sparse representations based on system size
- Graceful degradation: Operations fail cleanly with clear messages when limits exceeded
Test organization:
tests/test_matrix_operations.py: Matrix utilities
tests/test_exact_matrix.py: Exact matrix computation
tests/test_eigendecomposition.py: Eigenanalysis
tests/test_error_analysis.py: Error metrics
tests/test_analysis_integration.py: Update to include new features in full pipeline test
Dependencies Between Branches
main (bkk_numerical)
↓
bkk_config_full_analysis (rename example, establish baseline)
↓
bkk_exact_matrix (compute exact H matrix)
↓
bkk_eigendecomposition (eigenanalysis of both matrices)
↓
bkk_error_metrics (compare exact vs approximate)
↓
bkk_exact_simulation (simulate with exact matrix)
Each branch:
- Builds on previous work
- Can be merged independently as a complete feature
- Has its own tests and documentation
- Results in a working, testable system
Merge Request Checklist (for each branch)
Progress Tracking File
Location: /Users/bkkrueger/.claude/plans/exact_matrix_implementation_progress.md
This file will track:
- Current branch being worked on
- Completed branches and their merge commit hashes
- Any issues encountered and resolutions
- Notes for future work
- Branch dependencies and merge order
Format:
# Exact Matrix Implementation Progress
## Status: [In Progress / Paused / Complete]
Current branch: bkk_exact_matrix
Last updated: YYYY-MM-DD
## Completed Branches
- None yet
## In Progress: bkk_exact_matrix
- [x] Created branch from bkk_numerical
- [x] Added matrix_operations.py
- [ ] Unit tests for matrix operations
- [ ] Integration tests
- [ ] Documentation updates
- [ ] CI/CD verification
## Next Steps
1. Complete bkk_exact_matrix
2. Create MR for bkk_exact_matrix
3. Begin bkk_eigendecomposition
## Notes
- Remember to test with both small (2-3 qubit) and medium (4-6 qubit) systems
- Watch memory usage for large matrices
- Coordinate with team on merge timing
Critical Files Reference
Starting point: /Users/bkkrueger/research/quantum_computing/lisdi-qre/qhat/worktrees/bkk_numerical/
Key files to understand:
analysis/analysis.py:237-291: analyze_algorithm() main dispatcher
analysis/analysis.py:64-94: _compute_unitary_matrix() pattern to follow
analysis/analysis.py:152-234: numerical_simulation() pattern for exact simulation
analysis/config_types.py:157-168: AnalysisConfiguration class
analysis/file_io.py: save_matrix, load_state patterns to reuse
analysis/hamiltonian.py:132-169: get_all_pauli_strings() method
hamiltonian_generator/eigendecompose.py: Existing eigendecomposition code (for reference)
common/logging_utils.py: Centralized logging setup (new in main)
analysis/tests/conftest.py: Test logging configuration (new in main)
Logging patterns (after merging main into bkk_numerical):
# At top of module
import logging
logger = logging.getLogger(__name__)
# In functions
logger.info("Starting computation...") # replaces config_general.log()
logger.verbose("Detailed info...") # replaces config_general.log_verbose()
logger.debug("Debug info...") # replaces config_general.log_debug()
CI/CD:
.github/workflows/python-app.yml:80-90: analysis unit tests job
.gitlab-ci.yml:83-94: analysis tests job
Estimated Complexity
Branch 0 (config baseline): Very Low
- Simple rename and documentation update
- No new functionality, just organization
- Should be quick to complete
Branch 1 (exact matrix): Medium
- Core matrix operations are straightforward
- Testing with sparse matrices adds complexity
- Foundation for other branches
Branch 2 (eigendecomposition): Low-Medium
- scipy provides robust eigenvalue solvers
- Main complexity is handling different request modes
Branch 3 (error metrics): Medium
- Three different error types to implement
- State-dependent error requires careful coordination
- Memory management most critical here
Branch 4 (exact simulation): Low
- Largely copy of existing numerical_simulation()
- Integration and testing is main work
Scaling Summary by Analysis Type
| Analysis |
Small (≤15 qubits) |
Medium (16-20 qubits) |
Large (21-30 qubits) |
Notes |
| Exact matrix output |
✓ Dense |
✗ Too large |
✗ Too large |
Limited by dense matrix storage (16 GB at 15 qubits) |
| Approx matrix output |
✓ Dense |
✗ Too large |
✗ Too large |
Same limit as exact matrix |
| Eigendecomposition (full) |
✓ Dense |
✗ Too large |
✗ Too large |
Requires dense matrix |
| Eigendecomposition (partial) |
✓ Dense or sparse |
✓ Sparse |
✓ Sparse |
Matrix-free operator, scales well |
| Exact simulation |
✓ Dense |
✓ Matrix-free |
✓ Matrix-free |
State vector limit ~30 qubits (16 GB) |
| Approx simulation |
✓ Dense |
✓ Algorithm op |
✓ Algorithm op |
Same as exact simulation |
| Eigenvalue error |
✓ Dense |
✓ Sparse |
✓ Sparse |
Uses partial eigendecomposition |
| Matrix norm error |
✓ Dense |
⚠ Slow (matrix-free) |
⚠ Very slow (matrix-free) |
Frobenius: 2^N ops; Spectral: iterative |
| State error |
✓ Dense |
✓ Matrix-free |
✓ Matrix-free |
Best-scaling error metric |
Key takeaways:
- Dense operations limited to ~15 qubits (matrix output, full eigendecomp)
- Matrix norm errors work but slow for >15 qubits (minutes to hours for 20+ qubits)
- Partial eigendecomposition reaches 25-30 qubits via matrix-free operators
- Simulations and state errors reach 25-30 qubits (limited by state vector size)
- Production recommendation for 20+ qubits:
- Use partial eigendecomposition for eigenvalue comparison
- Use state errors for best performance
- Use matrix norms if needed, but expect long computation times
Success Criteria
Each branch:
- Passes all existing tests (no regressions)
- Passes new tests (>90% coverage for new code)
- Documentation complete
- CI/CD green on both platforms
Overall project:
- User can run full analysis comparing exact vs approximate
- Example config demonstrates all capabilities
- All error metrics validated on test systems
- No memory leaks with realistic problem sizes (20+ qubits)
- Performance validated at production scale:
- Confirm partial eigendecomposition works smoothly at 20 qubits
- Verify memory usage is reasonable for 20-qubit systems
- Document any system-size limitations discovered
- Ready for scientific use and publication
As a first pass (because I'm about to leave on vacation), I put together a plan with Claude and then let Claude run.
Merge requests:
Initial Prompt
Claude's Plan
Context
The qhat analysis tool currently supports:
This plan adds capabilities to compute exact Hamiltonian matrices (without Trotter or approximation), their eigendecompositions, and various error metrics comparing the approximate algorithm to the exact solution. These features are critical for validating algorithm accuracy and understanding the impact of approximations.
The work builds on the
bkk_numericalbranch, which introduced:_compute_unitary_matrix()numerical_simulation()analysis/analysis.pyRecent main branch updates (merged after bkk_numerical diverged):
common/logging_utils.pylogging.getLogger(__name__)patternmock_config.pyfrom tests in favor ofconftest.pywith logging fixturesconfig_types.py: Removed inline logging configuration (now handled bylogging_utils)analysis.py: All functions now use module-level logger instead ofconfig_general.log_*()bkk_numericalbefore proceeding with implementationArchitecture Overview
Current Structure
analyze_algorithm()Required Additions
Implementation Plan: Branch Structure
IMPORTANT: Merge Strategy
Before starting Branch 0, the
bkk_numericalbranch must be updated with the latest changes frommain. The main branch has undergone a significant logging refactor that affects:logging.getLogger(__name__))mock_config.py, addedconftest.py)Recommended approach:
mainintobkk_numerical:git merge mainanalysis/analysis.py: bkk_numerical added numerical simulation; main refactored logginganalysis/config_types.py: bkk_numerical added numerical_simulation_inputs; main refactored logginganalysis/file_io.py: bkk_numerical added state I/O; main refactored logginganalysis/hamiltonian.py: Both branches have minor changesanalysis/tests/: bkk_numerical added new tests; main added conftest.py and removed mock_config.pybkk_numericalcode to use new logging patterns:config_general.log()withlogger.info()config_general.log_verbose()withlogger.verbose()config_general.log_debug()withlogger.debug()conftest.pyinstead ofmock_config.pypython3.11 -m pytest analysis/tests/ -vExpected conflicts:
analysis.pyfunctions (numerical_simulation, output_unitary_matrix)test_numerical_simulation.py(references mock_config.py which was removed)Files unique to bkk_numerical (no conflicts, just keep them):
analysis/tests/test_numerical_simulation.pyanalysis/tests/test_analysis_integration.pyanalysis/README.mdsectionsAlternative approach (if merge is complex):
bkk_numericalonto currentmain:git rebase mainThe work is organized into 5 sequential branches (each builds on the updated
bkk_numerical):Branch 0:
bkk_config_full_analysis(Baseline Configuration)Purpose: Establish comprehensive example configuration that exercises all current QHAT analyses
Builds on:
bkk_numericalFiles to modify:
analysis/examples/config2.py→analysis/examples/config_full_analysis.pyanalysis/README.mdto referenceconfig_full_analysis.pyinstead ofconfig2.pyconfig2.pyKey implementation details:
Testing:
config_full_analysis.pyand verify all existing analyses workMerge criteria:
Branch 1:
bkk_exact_matrix(Foundation)Purpose: Compute exact Hamiltonian matrices without approximations
Builds on:
bkk_config_full_analysisFiles to modify:
analysis/analysis.py: Add_compute_exact_matrix()helper andexact_matrix_output()analysisanalysis/config_types.py: Addexact_matrix_output_filefield to AnalysisConfigurationanalysis/hamiltonian.py: Addto_matrix()method to Hamiltonian classanalysis/README.md: Document new exact matrix capabilityanalysis/examples/config_full_analysis.py: Add example of exact matrix output.github/workflows/python-app.yml: Ensure tests run.gitlab-ci.yml: Ensure tests runNew files:
analysis/matrix_operations.py: Utility functions for Pauli string operationspauli_char_to_matrix(): Convert 'I', 'X', 'Y', 'Z' to 2x2 matrices (for small systems)pauli_string_to_matrix(): Build full matrix from string (for small systems)pauli_dict_to_matrix(): Sum weighted Pauli strings into dense matrix (for small systems)PauliStringOperatorclass: Matrix-free linear operator for large systemsmatvec(v)to apply operator to vector without materializing matrixanalysis/tests/test_matrix_operations.py: Unit tests for matrix utilitiesanalysis/tests/test_exact_matrix.py: Integration tests for exact matrix computationKey implementation details:
Reuse
hamiltonian.get_all_pauli_strings(return_as='strings')to get coefficient dictTwo-path design based on system size:
Small systems (≤15 qubits): Dense path
pauli_dict_to_matrix()Large systems (>15 qubits): Matrix-free path
PauliStringOperatorthat stores Pauli stringsmatvec(v)for matrix-vector products without materializing matrixLinearOperatorinterfaceAutomatic selection: Check dimension, use dense if ≤32768 (15 qubits), else matrix-free
User override: Optional
force_denseorforce_sparseparametersMatrix output to file:
save_matrix()Key insight: Different analyses may need different representations - some cache, some compute on-the-fly
Testing:
Merge criteria:
Branch 2:
bkk_eigendecomposition(Eigenanalysis)Purpose: Compute eigendecompositions of exact and unitary matrices
Builds on:
bkk_exact_matrixFiles to modify:
analysis/analysis.py: Addeigendecomposition_analysis()functionanalysis/config_types.py: Add eigendecomposition config fields to AnalysisConfiguration:num_eigenvalues: int or str (number of eigenvalues to compute, default=0)eigendecomposition_matrices: str ('exact', 'approximate', or 'both', default='approximate')which_eigenvalues: str ('smallest', 'largest', or 'both', default='smallest')analysis/file_io.py: Addsave_eigendecomposition()andload_eigendecomposition()analysis/README.md: Document eigendecomposition analysisanalysis/examples/config_full_analysis.py: Add eigendecomposition configurationNew files:
analysis/tests/test_eigendecomposition.py: Tests for eigendecomposition functionalityKey implementation details:
num_eigenvalues != 0(either positive int or "all")eigendecomposition_matrices:'approximate'(default): Only compute approximate matrix and its eigendecomposition →approximate_eigendecomposition.npz'exact': Only compute exact matrix and its eigendecomposition →exact_eigendecomposition.npz'both': Compute both matrices and both eigendecompositions → two output filesMatrix representation strategy:
scipy.sparse.linalg.eigsh()accepts either:LinearOperatorwithmatvec(v)methodPauliStringOperatoror approximate algorithm's operator directly toeigshEigendecomposition modes:
Full eigendecomposition (
num_eigenvalues = "all"):scipy.linalg.eigh()to compute all eigenvalues/eigenvectorswhich_eigenvaluesparameter ignored (all eigenvalues computed anyway)Partial eigendecomposition (
num_eigenvalues > 0):scipy.sparse.linalg.eigsh()withkparameterwhich_eigenvaluessettingImplementation: Check
if isinstance(num_eigenvalues, str) and num_eigenvalues.lower() == "all"Memory management: eigsh only stores k eigenvectors (not full basis)
Output format for each npz file:
eigenvalues: 1D array of eigenvalueseigenvectors: 2D array (column i is eigenvector for eigenvalue i)matrix_type: 'exact' or 'approximate'num_eigenvalues: number computedwhich_eigenvalues: 'smallest', 'largest', or 'both'Implementation note: Map user-friendly values to scipy's
whichparameter:Documentation must emphasize: "smallest" means algebraically smallest (closest to -∞), not smallest magnitude
Memory management: Store only requested number of eigenvalues/vectors
Testing:
num_eigenvalues = "all"(full decomposition) on small matrixnum_eigenvalues = 5(partial decomposition) on larger matrixeigendecomposition_matrices='exact'(one output file)eigendecomposition_matrices='approximate'(one output file)eigendecomposition_matrices='both'(two output files)which_eigenvaluesBranch 3:
bkk_error_metrics(Error Analysis)Purpose: Compute three error metrics comparing exact vs approximate
Builds on:
bkk_eigendecompositionFiles to modify:
analysis/analysis.py: Adderror_analysis()functionanalysis/config_types.py: Add error analysis fields to AnalysisConfiguration:error_num_eigenvalues: int (how many eigenvalues to compare; 0 = skip eigenvalue errors, default=0)error_matrix_norms: str or list of str (which matrix norms to compute; None = skip matrix errors, default=None)error_state_inputs: str or list of str (state files for error comparison; None = skip state errors, default=None)analysis/README.md: Document error analysesanalysis/examples/config_full_analysis.py: Add error analysis configurationNew files:
analysis/tests/test_error_analysis.py: Tests for error metricsKey implementation details:
Error analysis has three independent components, each explicitly enabled:
Eigenvalue Error (enabled when
error_num_eigenvalues > 0):eigenvalue_errorsarray with differencesexact_eigs[i] - approx_eigs[i]error_num_eigenvalues = 1for ground state energy comparisonMatrix Norm Error (enabled when
error_matrix_normsis not None):||U_exact - U_approximate||Small systems (≤15 qubits): Direct computation
diff = exact - approximateLarge systems (>15 qubits): Matrix-free computation (expensive but feasible)
Frobenius norm: Can compute without materializing full matrices
||A||_F^2 = sum_ij |A_ij|^2 = trace(A† A)Spectral norm: Very expensive but computable
||A||_2 = largest singular value = sqrt(largest eigenvalue of A† A)Implementation approach for Branch 3:
Small systems (≤15 qubits): Direct computation with dense matrices (fast)
Large systems (>15 qubits): Matrix-free computation with progress warnings
User can always Ctrl+C if it's too slow
Suggest state-dependent errors as faster alternative in documentation
Frobenius norm ("frobenius"):
||A||_F = sqrt(sum of squares of all elements)Spectral norm ("spectral"):
||A||_2 = largest singular valueNormalize input to list internally (handle both string and list inputs)
State-Dependent Error (enabled when
error_state_inputsis not None):approx_final = approx_operator @ initial_stateexact_final = exact_operator @ initial_state||exact_final - approx_final||_2numerical_simulation_inputsandexact_simulation_inputsOutput format (npz or JSON file, e.g.,
error_analysis.npz):eigenvalue_errors: Array of eigenvalue differences (if eigenvalue error enabled)eigenvalue_errors_relative: Relative errors (if eigenvalue error enabled)matrix_frobenius_error: Scalar (if "frobenius" in error_matrix_norms)matrix_spectral_error: Scalar (if "spectral" in error_matrix_norms)state_errors: List of dicts with {input_file, absolute_error, relative_error} (if state error enabled)Memory management:
analyze_algorithm()and explicitly delete large arraysTesting:
error_matrix_normsas string ("frobenius") and as list (["frobenius", "spectral"])Branch 4:
bkk_exact_simulation(Complete Pipeline)Purpose: Numerical simulation using exact matrix
Builds on:
bkk_error_metricsFiles to modify:
analysis/analysis.py: Addexact_numerical_simulation()functionanalysis/config_types.py: Add to AnalysisConfiguration:exact_simulation_inputs: string or list (like numerical_simulation_inputs)analysis/README.md: Document exact simulationanalysis/examples/config_full_analysis.py: Update to demonstrate all analyses togetherKey implementation details:
numerical_simulation()functioninput_exact_final.npy(distinguish frominput_final.npy)Testing:
Cross-Cutting Concerns
Memory Management Pattern
Key architectural change for scale: Don't always materialize full matrices. Use operators (dense matrix or matrix-free) depending on system size.
Design principle: Analyses accept operators that may be dense matrices OR LinearOperators with matvec(). Each analysis adapts its implementation based on operator type.
Configuration Validation
analyze_algorithm()validates at least one analysis is requestedCI/CD Updates
Both
.github/workflows/python-app.ymland.gitlab-ci.yml:run-analysis-testsjobs that run pytestpytest tests/Documentation Strategy
README.md sections to add/update:
num_eigenvaluesoptions:analysis.num_eigenvalues = "all"oranalysis.num_eigenvalues = 5Code comments:
Example updates:
config_full_analysis.py(renamed in Branch 0) evolves across branches to demonstrate cumulative featuresTesting Strategy
Unit tests:
Integration tests:
Production scale expectations:
Scaling limits by operation type:
Design principles for maximum scale:
Test organization:
tests/test_matrix_operations.py: Matrix utilitiestests/test_exact_matrix.py: Exact matrix computationtests/test_eigendecomposition.py: Eigenanalysistests/test_error_analysis.py: Error metricstests/test_analysis_integration.py: Update to include new features in full pipeline testDependencies Between Branches
Each branch:
Merge Request Checklist (for each branch)
Progress Tracking File
Location:
/Users/bkkrueger/.claude/plans/exact_matrix_implementation_progress.mdThis file will track:
Format:
Critical Files Reference
Starting point:
/Users/bkkrueger/research/quantum_computing/lisdi-qre/qhat/worktrees/bkk_numerical/Key files to understand:
analysis/analysis.py:237-291:analyze_algorithm()main dispatcheranalysis/analysis.py:64-94:_compute_unitary_matrix()pattern to followanalysis/analysis.py:152-234:numerical_simulation()pattern for exact simulationanalysis/config_types.py:157-168: AnalysisConfiguration classanalysis/file_io.py: save_matrix, load_state patterns to reuseanalysis/hamiltonian.py:132-169: get_all_pauli_strings() methodhamiltonian_generator/eigendecompose.py: Existing eigendecomposition code (for reference)common/logging_utils.py: Centralized logging setup (new in main)analysis/tests/conftest.py: Test logging configuration (new in main)Logging patterns (after merging main into bkk_numerical):
CI/CD:
.github/workflows/python-app.yml:80-90: analysis unit tests job.gitlab-ci.yml:83-94: analysis tests jobEstimated Complexity
Branch 0 (config baseline): Very Low
Branch 1 (exact matrix): Medium
Branch 2 (eigendecomposition): Low-Medium
Branch 3 (error metrics): Medium
Branch 4 (exact simulation): Low
Scaling Summary by Analysis Type
Key takeaways:
Success Criteria
Each branch:
Overall project: