diff --git a/AGENTS.md b/AGENTS.md index 06e7dd7..dcb3560 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -331,17 +331,24 @@ qvartools/ ├── experiments/ # Reproducible experiment scripts │ ├── config_loader.py # YAML loader with CLI overrides (--config, --device) │ ├── profile_pipeline.py # Wall-clock profiling of pipeline stages -│ └── pipelines/ # 24 end-to-end pipeline scripts (8 groups × 3 diag modes) -│ ├── run_all_pipelines.py # Run all 24 pipelines and compare results -│ ├── configs/ # 8 YAML config files (one per group) -│ ├── 01_dci/ # Direct-CI (no NF): classical, quantum, SQD -│ ├── 02_nf_dci/ # NF + DCI merge: classical, quantum, SQD -│ ├── 03_nf_dci_pt2/ # NF + DCI + PT2 expansion: classical, quantum, SQD -│ ├── 04_nf_only/ # NF-only ablation: classical, quantum, SQD -│ ├── 05_hf_only/ # HF-only baseline: classical, quantum, SQD -│ ├── 06_iterative_nqs/ # Iterative NQS: classical, quantum, SQD -│ ├── 07_iterative_nqs_dci/ # NF+DCI merge → iterative NQS: classical, quantum, SQD -│ └── 08_iterative_nqs_dci_pt2/ # NF+DCI+PT2 → iterative NQS: classical, quantum, SQD +│ └── pipelines/ # 33 end-to-end pipeline scripts (3-digit prefix catalog) +│ ├── run_all_pipelines.py # Run all 33 pipelines and compare results +│ ├── configs/ # 13 YAML configs (9 ablation + 4 method-as-pipeline) +│ │ +│ ├── 001_dci/ # Direct-CI (no NF): classical, quantum, SQD +│ ├── 002_nf_dci/ # NF + DCI merge: classical, quantum, SQD +│ ├── 003_nf_dci_pt2/ # NF + DCI + PT2 expansion: classical, quantum, SQD +│ ├── 004_nf_only/ # NF-only ablation: classical, quantum, SQD +│ ├── 005_hf_only/ # HF-only baseline: classical, quantum, SQD +│ ├── 006_iterative_nqs/ # Iterative NQS: classical, quantum, SQD +│ ├── 007_iterative_nqs_dci/ # NF+DCI merge → iterative NQS +│ ├── 008_iterative_nqs_dci_pt2/ # NF+DCI+PT2 → iterative NQS +│ ├── 009_vqe/ # CUDA-QX VQE: UCCSD, ADAPT-VQE +│ │ +│ ├── 010_hi_nqs_sqd/ # HI+NQS+SQD: default, pt2, ibm_off +│ ├── 011_hi_nqs_skqd/ # HI+NQS+SKQD: default, ibm_on +│ ├── 012_nqs_sqd/ # NQS+SQD: default +│ └── 013_nqs_skqd/ # NQS+SKQD: default │ ├── docs/ # Documentation │ ├── architecture.md # Design philosophy, module dependency graph @@ -599,18 +606,22 @@ pytest --cov=qvartools --cov-report=term-missing ### Config Loader Pattern -All 24 pipeline scripts use the shared `config_loader.py`: +All 33 pipeline scripts use the shared `config_loader.py`: ```bash -python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py h2 --device cuda +python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py h2 --device cuda python experiments/pipelines/run_all_pipelines.py h2 --device cuda -python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py lih \ - --config experiments/pipelines/configs/02_nf_dci.yaml --max-epochs 200 +python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py lih \ + --config experiments/pipelines/configs/002_nf_dci.yaml --max-epochs 200 + +# New 010-013 method-as-pipeline catalog +python experiments/pipelines/010_hi_nqs_sqd/default.py h2 --device cuda +python experiments/pipelines/010_hi_nqs_sqd/pt2.py h2 --device cuda ``` **Precedence:** CLI args > YAML file > hardcoded defaults. -### 24 Pipeline Variants (8 Groups × 3 Diag Modes) +### 33 Pipeline Variants (9 ablation groups + 4 method-as-pipeline groups) | Group | Basis Source | Classical Krylov | Quantum Krylov | SQD | |-------|-------------|-----------------|----------------|-----| diff --git a/CHANGELOG.md b/CHANGELOG.md index ec32eb4..a25e245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- **BREAKING**: `experiments/pipelines/` folders renamed from 2-digit to 3-digit prefix + (`01_dci` → `001_dci`, ..., `09_vqe` → `009_vqe`). YAML configs in + `experiments/pipelines/configs/` renamed to match. Leaves room for the 010-099 + method-as-pipeline catalog tier. +- **BREAKING**: `run_all_pipelines.py --only` argument now requires 3-digit group + prefixes (e.g., `--only 001 002 004`). Passing 2-digit values like `--only 01 02` + silently matched no groups; now emits a migration warning with the recommended + 3-digit form. Scripts, docs, and `docs/experiments_guide.md` updated. - **BREAKING**: Rename `SampleBasedKrylovDiagonalization` to `ClassicalKrylovDiagonalization` (ADR-001) - **BREAKING**: Rename `FlowGuidedSKQD` to `FlowGuidedKrylovDiag` (ADR-001) - **BREAKING**: Default `subspace_mode` changed from `"skqd"` to `"classical_krylov"` @@ -17,6 +25,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `FCISolver._dense_fallback()` returns `None` instead of raising `RuntimeError` for large Hilbert spaces ### Added +- **Pipeline catalog Tier 2**: 4 new pipeline folders (`010_hi_nqs_sqd`, + `011_hi_nqs_skqd`, `012_nqs_sqd`, `013_nqs_skqd`) wrapping the + `qvartools.methods.nqs.*` runners as first-class benchmark catalog entries. + Total pipeline scripts: 26 → 33. Each method gets a folder; variants live + as separate scripts inside the folder with a multi-section YAML config. +- `qvartools.methods.nqs.METHODS_REGISTRY`: public dict keyed by method id + (`"nqs_sqd"`, `"nqs_skqd"`, `"hi_nqs_sqd"`, `"hi_nqs_skqd"`) mapping to + runner function, config class, capability flags, and pipeline folder + metadata. Used by 010-013 wrappers and available for benchmark harnesses. +- `src/qvartools/methods/nqs/_shared.py`: internal module with + `build_autoregressive_nqs`, `extract_orbital_counts`, + `validate_initial_basis` helpers extracted from the four NQS method + modules to remove duplication. +- `experiments.config_loader.get_explicit_cli_args`: the previously + private `_get_explicit_cli_args` is now the public entry point under + this name (the leading-underscore alias is kept for backward + compatibility with existing callers). Used by the 010-013 wrapper + scripts to detect which CLI args were explicitly typed, so YAML + section defaults actually apply when `--device`/molecule is omitted. - `compute_molecular_integrals` now accepts `cas` and `casci` parameters for CAS active-space reduction - 14 new CAS molecules in registry (26 total): N₂-CAS(10,12/15/17/20/26), Cr₂ + variants up to 72Q, Benzene CAS(6,15) - IBM `solve_fermion` auto-enabled when `qiskit_addon_sqd` is installed (α×β Cartesian product, dramatically better accuracy) @@ -48,6 +75,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - S-CORE (`recover_configurations`) from HI-NQS-SQD IBM path — designed for quantum hardware noise, not needed for classical NQS samples (NH₃ 1.5 hr → 5 s) ### Fixed +- `nqs_sqd.py` and `nqs_skqd.py` were end-to-end broken: they accessed + `mol_info["n_orbitals"]` directly, but `get_molecule()` does not populate + that key. Routed through the new `extract_orbital_counts()` helper which + falls back to `hamiltonian.integrals` (same fallback logic the HI methods + already had). Both runners now smoke-tested on H₂. - `TransformerNFSampler._build_nqs()` used wrong parameter name `hidden_dim` instead of `hidden_dims` - `hi_nqs_sqd.py` passed tensors instead of numpy arrays to `vectorized_dedup` - Groups 07/08 pipelines discarded NF+DCI basis when calling iterative NQS solvers (Issue #10) diff --git a/README.md b/README.md index d6b1759..f42278d 100644 --- a/README.md +++ b/README.md @@ -100,18 +100,25 @@ print(f"Energy: {results['final_energy']:.10f} Ha") ### Running experiment pipelines ```bash -# Run a single pipeline on H2 -python experiments/pipelines/01_dci/dci_krylov_classical.py h2 --device cuda +# Run a single ablation pipeline on H2 +python experiments/pipelines/001_dci/dci_krylov_classical.py h2 --device cuda -# Run all 24 pipelines and compare +# Run a method-as-pipeline benchmark (010+ catalog) +python experiments/pipelines/010_hi_nqs_sqd/default.py h2 --device cuda +python experiments/pipelines/010_hi_nqs_sqd/pt2.py h2 --device cuda + +# Run all 33 pipelines and compare python experiments/pipelines/run_all_pipelines.py h2 --device cuda +# Filter by group prefix (3-digit) +python experiments/pipelines/run_all_pipelines.py h2 --only 001 005 010 + # Skip quantum or iterative pipelines for faster validation python experiments/pipelines/run_all_pipelines.py h2 --skip-quantum --skip-iterative # Run with a YAML config override -python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py lih \ - --config experiments/pipelines/configs/02_nf_dci.yaml --max-epochs 200 +python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py lih \ + --config experiments/pipelines/configs/002_nf_dci.yaml --max-epochs 200 ``` ## Package Architecture diff --git a/docs/decisions/002-eliminate-torch-numpy-roundtrips.md b/docs/decisions/002-eliminate-torch-numpy-roundtrips.md index 50edaba..e5d0928 100644 --- a/docs/decisions/002-eliminate-torch-numpy-roundtrips.md +++ b/docs/decisions/002-eliminate-torch-numpy-roundtrips.md @@ -282,12 +282,12 @@ ruff format --check src/ tests/ experiments/ # 5. End-to-end pipeline validation (CRITICAL) cd experiments/pipelines python run_all_pipelines.py h2 --device cuda -# Expected: 24/24 pipelines pass +# Expected: 33/33 pipelines pass (post-2026-04-07 catalog with 010-013) # 6. Performance regression check # Run the specific pipeline before and after, compare wall time -python pipelines/01_dci/dci_sqd.py h2 --device cuda # PR-A -python pipelines/01_dci/dci_krylov_classical.py h2 --device cuda # PR-B +python pipelines/001_dci/dci_sqd.py h2 --device cuda # PR-A +python pipelines/001_dci/dci_krylov_classical.py h2 --device cuda # PR-B ``` --- diff --git a/docs/decisions/003-gpu-native-sbd-integration.md b/docs/decisions/003-gpu-native-sbd-integration.md index 8c99a08..e0d8057 100644 --- a/docs/decisions/003-gpu-native-sbd-integration.md +++ b/docs/decisions/003-gpu-native-sbd-integration.md @@ -146,6 +146,42 @@ nvcc -std=c++17 -O3 \ # ARM64: no cross-compilation needed (native on DGX Spark) ``` +## Update 2026-04-07: parallel CuPy path + correction on speedup interpretation + +**Correction to the speedup table above**: re-reading arXiv:2601.16169 carefully, +AMD-HPC/amd-sbd's 95x is measured on **MI250X (AMD GPU, OpenMP offload)**. +On **GB200** the same library only achieves **2.64x** — a 36x gap between the +two numbers in the same paper. The "95x" cannot be naively claimed for any +NVIDIA target. For DGX Spark GB10 (weaker than GB200 in both bandwidth and +compute), realistic expectation from amd-sbd is **≤2.64x or less**. This +significantly narrows the speedup advantage of the C++/sbd path over a +well-written native Python (CuPy) implementation on NVIDIA hardware. + +A pure-CuPy alternative path (factored-space sigma vector + Davidson driver) +is now tracked in **Issue #38**. Motivations: + +1. **No C++ build / MPI strip / sm_121 compilation risk** — pure-Python, runs + wherever CuPy works. Eliminates the entire toolchain risk surface from this + ADR. +2. **Davidson with diagonal preconditioner is non-negotiable for multireference + chemistry** (Cr₂ CAS(12,18) through CAS(12,36), N₂ dissociation, open-shell). + Lanczos has ghost-eigenvalue failure modes for near-degenerate spectra; + PySCF uses Davidson for FCI for this reason. **Whichever backend wins (sbd + or CuPy), proper Davidson must be implemented before Cr₂ work begins.** + This commitment is also recorded in `memory/project_phase2b_davidson_commitment.md`. +3. **Insurance path**: if sbd nanobind Phase 2 hits compilation blockers + (CUDA 13.0 CCCL relocation, MPI strip, sm_121 SASS compatibility), Issue + #38 can carry the long-term work independently. + +The sbd path described in this ADR remains active — Issue #38 is **parallel, +not a replacement**. Re-evaluate priorities once both Phase 1 prototypes have +measured numbers on actual DGX Spark GB10 hardware. + +**Cross-references added 2026-04-07**: +- Issue #38: CuPy factored-space Davidson tracking (parallel path) +- `memory/project_phase2b_davidson_commitment.md`: Phase 2b non-negotiable commitment +- `memory/feedback_gpu_sku_extrapolation.md`: lesson on misreading vendor speedup claims + ## References - r-ccs-cms/sbd: https://github.com/r-ccs-cms/sbd diff --git a/docs/experiments_guide.md b/docs/experiments_guide.md index 569d1bc..6c5d943 100644 --- a/docs/experiments_guide.md +++ b/docs/experiments_guide.md @@ -24,96 +24,162 @@ All scripts accept: ## Pipeline Groups -### Group 01: Direct-CI (no NF training) +### Group 001: Direct-CI (no NF training) Generates HF + singles + doubles deterministically, then diagonalizes. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `01_dci/dci_krylov_classical.py` | Classical Krylov | DCI -> SKQD time evolution | -| `01_dci/dci_krylov_quantum.py` | Quantum Krylov | DCI -> Trotterized circuit evolution | -| `01_dci/dci_sqd.py` | SQD | DCI -> noise + S-CORE batch diag | +| `001_dci/dci_krylov_classical.py` | Classical Krylov | DCI -> SKQD time evolution | +| `001_dci/dci_krylov_quantum.py` | Quantum Krylov | DCI -> Trotterized circuit evolution | +| `001_dci/dci_sqd.py` | SQD | DCI -> noise + S-CORE batch diag | -### Group 02: NF-NQS + DCI Merge +### Group 002: NF-NQS + DCI Merge Trains a normalizing flow, merges NF-sampled basis with Direct-CI essentials. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `02_nf_dci/nf_dci_krylov_classical.py` | Classical Krylov | NF+DCI merge -> SKQD | -| `02_nf_dci/nf_dci_krylov_quantum.py` | Quantum Krylov | NF+DCI merge -> Trotterized | -| `02_nf_dci/nf_dci_sqd.py` | SQD | NF+DCI merge -> noise + S-CORE | +| `002_nf_dci/nf_dci_krylov_classical.py` | Classical Krylov | NF+DCI merge -> SKQD | +| `002_nf_dci/nf_dci_krylov_quantum.py` | Quantum Krylov | NF+DCI merge -> Trotterized | +| `002_nf_dci/nf_dci_sqd.py` | SQD | NF+DCI merge -> noise + S-CORE | -### Group 03: NF + DCI + PT2 Expansion +### Group 003: NF + DCI + PT2 Expansion Same as Group 02, plus CIPSI-style perturbative basis expansion via Hamiltonian connections. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `03_nf_dci_pt2/nf_dci_pt2_krylov_classical.py` | Classical Krylov | NF+DCI+PT2 -> SKQD | -| `03_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py` | Quantum Krylov | NF+DCI+PT2 -> Trotterized | -| `03_nf_dci_pt2/nf_dci_pt2_sqd.py` | SQD | NF+DCI+PT2 -> noise + S-CORE | +| `003_nf_dci_pt2/nf_dci_pt2_krylov_classical.py` | Classical Krylov | NF+DCI+PT2 -> SKQD | +| `003_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py` | Quantum Krylov | NF+DCI+PT2 -> Trotterized | +| `003_nf_dci_pt2/nf_dci_pt2_sqd.py` | SQD | NF+DCI+PT2 -> noise + S-CORE | -### Group 04: NF-Only (Ablation) +### Group 004: NF-Only (Ablation) NF training without DCI scaffolding. Tests pure NF generative power. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `04_nf_only/nf_krylov_classical.py` | Classical Krylov | NF-only -> SKQD | -| `04_nf_only/nf_krylov_quantum.py` | Quantum Krylov | NF-only -> Trotterized | -| `04_nf_only/nf_sqd.py` | SQD | NF-only -> noise + S-CORE | +| `004_nf_only/nf_krylov_classical.py` | Classical Krylov | NF-only -> SKQD | +| `004_nf_only/nf_krylov_quantum.py` | Quantum Krylov | NF-only -> Trotterized | +| `004_nf_only/nf_sqd.py` | SQD | NF-only -> noise + S-CORE | -### Group 05: HF-Only (Baseline) +### Group 005: HF-Only (Baseline) Minimal baseline starting from a single Hartree-Fock reference state. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `05_hf_only/hf_krylov_classical.py` | Classical Krylov | HF -> Krylov discovers configs | -| `05_hf_only/hf_krylov_quantum.py` | Quantum Krylov | HF -> Trotterized circuit | -| `05_hf_only/hf_sqd.py` | SQD | HF -> noise + S-CORE | +| `005_hf_only/hf_krylov_classical.py` | Classical Krylov | HF -> Krylov discovers configs | +| `005_hf_only/hf_krylov_quantum.py` | Quantum Krylov | HF -> Trotterized circuit | +| `005_hf_only/hf_sqd.py` | SQD | HF -> noise + S-CORE | -### Group 06: Iterative NQS +### Group 006: Iterative NQS Iterative autoregressive transformer NQS with eigenvector feedback. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `06_iterative_nqs/iter_nqs_krylov_classical.py` | Classical Krylov | NQS loop + H-connection expansion | -| `06_iterative_nqs/iter_nqs_krylov_quantum.py` | Quantum Krylov | NQS warmup + Trotterized | -| `06_iterative_nqs/iter_nqs_sqd.py` | SQD | NQS loop + batch diag | +| `006_iterative_nqs/iter_nqs_krylov_classical.py` | Classical Krylov | NQS loop + H-connection expansion | +| `006_iterative_nqs/iter_nqs_krylov_quantum.py` | Quantum Krylov | NQS warmup + Trotterized | +| `006_iterative_nqs/iter_nqs_sqd.py` | SQD | NQS loop + batch diag | -### Group 07: NF + DCI Merge -> Iterative NQS +### Group 007: NF + DCI Merge -> Iterative NQS NF training and DCI merge (same as Group 02 stages 1-2), then iterative NQS refinement. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `07_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py` | Classical Krylov | NF+DCI -> iterative NQS+Krylov | -| `07_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py` | Quantum Krylov | NF+DCI -> quantum Krylov | -| `07_iterative_nqs_dci/iter_nqs_dci_sqd.py` | SQD | NF+DCI -> iterative NQS+SQD | +| `007_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py` | Classical Krylov | NF+DCI -> iterative NQS+Krylov | +| `007_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py` | Quantum Krylov | NF+DCI -> quantum Krylov | +| `007_iterative_nqs_dci/iter_nqs_dci_sqd.py` | SQD | NF+DCI -> iterative NQS+SQD | -### Group 08: NF + DCI + PT2 -> Iterative NQS +### Group 008: NF + DCI + PT2 -> Iterative NQS NF training, DCI merge, and PT2 expansion (same as Group 03 stages 1-2.5), then iterative NQS. | Script | Diag Mode | Description | |--------|-----------|-------------| -| `08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py` | Classical Krylov | NF+DCI+PT2 -> iterative NQS+Krylov | -| `08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py` | Quantum Krylov | NF+DCI+PT2 -> quantum Krylov | -| `08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py` | SQD | NF+DCI+PT2 -> iterative NQS+SQD | +| `008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py` | Classical Krylov | NF+DCI+PT2 -> iterative NQS+Krylov | +| `008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py` | Quantum Krylov | NF+DCI+PT2 -> quantum Krylov | +| `008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py` | SQD | NF+DCI+PT2 -> iterative NQS+SQD | + +--- + +## Tier 2: Method-as-Pipeline Catalog (010-099) + +Each NQS method from `src/qvartools/methods/nqs/` is exposed as a runnable +benchmark folder. Variants of the same method live as separate scripts inside +the folder, with one multi-section YAML config per method (each section +corresponds to one variant script). + +### Group 010: HI+NQS+SQD + +Iterative HI loop with optional PT2 selection and IBM solver. + +| Script | Variant | Description | +|--------|---------|-------------| +| `010_hi_nqs_sqd/default.py` | default | Baseline HI+NQS+SQD with auto IBM detect | +| `010_hi_nqs_sqd/pt2.py` | pt2 | `use_pt2_selection=True` + temperature annealing | +| `010_hi_nqs_sqd/ibm_off.py` | ibm_off | `use_ibm_solver=False` (force GPU fallback) | + +Backed by `qvartools.methods.nqs.run_hi_nqs_sqd`. +Config: `experiments/pipelines/configs/010_hi_nqs_sqd.yaml` (3 sections). + +### Group 011: HI+NQS+SKQD + +Iterative HI loop with Krylov expansion. + +| Script | Variant | Description | +|--------|---------|-------------| +| `011_hi_nqs_skqd/default.py` | default | Baseline HI+NQS+SKQD with Krylov expansion | +| `011_hi_nqs_skqd/ibm_on.py` | ibm_on | `use_ibm_solver=True` + S-CORE recovery | + +Backed by `qvartools.methods.nqs.run_hi_nqs_skqd`. +Config: `experiments/pipelines/configs/011_hi_nqs_skqd.yaml` (2 sections). + +### Group 012: NQS+SQD + +Two-stage NQS+SQD (no iteration). + +| Script | Variant | Description | +|--------|---------|-------------| +| `012_nqs_sqd/default.py` | default | Train NQS via VMC, sample, diagonalize | + +Backed by `qvartools.methods.nqs.run_nqs_sqd`. +Config: `experiments/pipelines/configs/012_nqs_sqd.yaml`. + +### Group 013: NQS+SKQD + +Two-stage NQS+SKQD with Krylov expansion. + +| Script | Variant | Description | +|--------|---------|-------------| +| `013_nqs_skqd/default.py` | default | Train NQS, sample, Krylov expand, diagonalize | + +Backed by `qvartools.methods.nqs.run_nqs_skqd`. +Config: `experiments/pipelines/configs/013_nqs_skqd.yaml`. + +### Numbering convention for future expansion + +- **001-009**: ablation pipeline groups (current) +- **010-099**: method-as-pipeline catalog (4 used: 010-013, 86 free) +- **100-199**: cross-method sweeps (e.g., `100_h2_all_methods`) +- **200+**: hyperparameter sweeps (e.g., `200_hi_nqs_sqd_lr_sweep`) + +The full method registry is exposed at runtime via +`qvartools.methods.nqs.METHODS_REGISTRY` for programmatic dispatch. --- ## Running All Pipelines ```bash -# Run all 24 pipelines on H2 and compare +# Run all 33 pipelines on H2 and compare python experiments/pipelines/run_all_pipelines.py h2 --device cuda # Run only specific groups -python experiments/pipelines/run_all_pipelines.py h2 --only 01 02 04 +python experiments/pipelines/run_all_pipelines.py h2 --only 001 002 004 # Skip quantum pipelines (no CUDA-Q needed) python experiments/pipelines/run_all_pipelines.py h2 --skip-quantum diff --git a/docs/source/tutorials/beh2_pipeline.rst b/docs/source/tutorials/beh2_pipeline.rst index d78968b..6a37f4c 100644 --- a/docs/source/tutorials/beh2_pipeline.rst +++ b/docs/source/tutorials/beh2_pipeline.rst @@ -83,12 +83,12 @@ Using YAML Configs .. code-block:: bash # Run from the command line - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py beh2 --device cuda - python experiments/pipelines/01_dci/dci_krylov_classical.py beh2 --device cuda + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py beh2 --device cuda + python experiments/pipelines/001_dci/dci_krylov_classical.py beh2 --device cuda # Or with YAML config overrides - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py beh2 \ - --config experiments/pipelines/configs/02_nf_dci.yaml + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py beh2 \ + --config experiments/pipelines/configs/002_nf_dci.yaml Interpreting Results -------------------- diff --git a/docs/source/tutorials/h2_pipeline.rst b/docs/source/tutorials/h2_pipeline.rst index 1c92e2a..0a597cd 100644 --- a/docs/source/tutorials/h2_pipeline.rst +++ b/docs/source/tutorials/h2_pipeline.rst @@ -115,11 +115,14 @@ The same pipeline can be run as a standalone experiment script: .. code-block:: bash - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py h2 --device cuda + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py h2 --device cuda # Or with a YAML config - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py h2 \ - --config experiments/pipelines/configs/02_nf_dci.yaml + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py h2 \ + --config experiments/pipelines/configs/002_nf_dci.yaml - # Run all 24 pipelines and compare + # Run all 33 pipelines (001-013) and compare python experiments/pipelines/run_all_pipelines.py h2 --device cuda + + # Or run just the new method-as-pipeline catalog (010-013) + python experiments/pipelines/run_all_pipelines.py h2 --only 010 011 012 013 diff --git a/docs/source/user_guide/pipelines.rst b/docs/source/user_guide/pipelines.rst index 4b05ade..e3d9377 100644 --- a/docs/source/user_guide/pipelines.rst +++ b/docs/source/user_guide/pipelines.rst @@ -1,57 +1,141 @@ Pipeline Methods ================ -qvartools provides 24 pipeline methods organized in 8 groups. Each group -combines a basis generation strategy (rows) with one of three diagonalization -modes (columns): Classical Krylov, Quantum Circuit Krylov, or SQD. +qvartools provides a standardized **benchmark catalog** of pipeline methods +under ``experiments/pipelines/``. The catalog uses 3-digit folder prefixes +so future methods can be added without renumbering. There are currently +**33 runnable pipeline scripts** organized in two tiers: -Pipeline Overview ------------------ +* **001-009** — Ablation groups (basis-generation strategies × diagonalization modes) +* **010-099** — Method-as-pipeline catalog (each NQS method as a first-class entry) + +Tier 1 — Ablation groups (001-009) +---------------------------------- + +Eight ablation groups combine a basis generation strategy with one of three +diagonalization modes (Classical Krylov / Quantum Krylov / SQD), plus a +ninth group for VQE. .. list-table:: :header-rows: 1 - :widths: 25 12 12 51 + :widths: 28 12 12 48 * - Group - NF Training - Diag Modes - Description - * - 01 DCI + * - 001 DCI - No - C / Q / SQD - Deterministic HF + singles + doubles - * - 02 NF+DCI + * - 002 NF+DCI - Yes - C / Q / SQD - NF training + DCI merge - * - 03 NF+DCI+PT2 + * - 003 NF+DCI+PT2 - Yes - C / Q / SQD - NF + DCI + perturbative expansion - * - 04 NF-Only + * - 004 NF-Only - Yes - C / Q / SQD - NF-only basis (ablation, no DCI) - * - 05 HF-Only + * - 005 HF-Only - No - C / Q / SQD - Single HF reference state (baseline) - * - 06 Iterative NQS + * - 006 Iterative NQS - Iterative - C / Q / SQD - Autoregressive NQS with eigenvector feedback - * - 07 NF+DCI -> Iter NQS + * - 007 NF+DCI -> Iter NQS - Yes + Iterative - C / Q / SQD - NF+DCI merge then iterative NQS refinement - * - 08 NF+DCI+PT2 -> Iter NQS + * - 008 NF+DCI+PT2 -> Iter NQS - Yes + Iterative - C / Q / SQD - NF+DCI+PT2 then iterative NQS refinement + * - 009 VQE + - No + - — + - CUDA-QX VQE (UCCSD + ADAPT-VQE) **Diag mode key:** C = Classical Krylov (SKQD), Q = Quantum Circuit Krylov (Trotterized), SQD = batch diag with noise + S-CORE. +Tier 2 — Method-as-pipeline catalog (010-013) +--------------------------------------------- + +Each NQS method from ``src/qvartools/methods/nqs/`` is exposed as a runnable +benchmark folder. Variants of the same method live as separate scripts inside +the folder, with one multi-section YAML config per method. + +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - Pipeline folder + - Variants + - Method (``qvartools.methods.nqs``) + * - ``010_hi_nqs_sqd`` + - ``default``, ``pt2``, ``ibm_off`` + - ``run_hi_nqs_sqd`` — iterative HI loop, optional PT2 selection / IBM solver + * - ``011_hi_nqs_skqd`` + - ``default``, ``ibm_on`` + - ``run_hi_nqs_skqd`` — iterative HI loop with Krylov expansion + * - ``012_nqs_sqd`` + - ``default`` + - ``run_nqs_sqd`` — two-stage NQS+SQD (no iteration) + * - ``013_nqs_skqd`` + - ``default`` + - ``run_nqs_skqd`` — two-stage NQS+SKQD with Krylov expansion + +Programmatic dispatch via ``METHODS_REGISTRY`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The four methods are also exposed at runtime as +:data:`qvartools.methods.nqs.METHODS_REGISTRY`, a dict keyed by a stable +method id (``"nqs_sqd"``, ``"nqs_skqd"``, ``"hi_nqs_sqd"``, ``"hi_nqs_skqd"``). +Each value is a sub-dict containing: + +* ``run_fn`` — the runner function (e.g. ``run_hi_nqs_sqd``) +* ``config_cls`` — the frozen ``dataclass`` config type +* ``iterative`` — whether the method is an iterative HI loop +* ``has_krylov_expansion`` — whether the basis is grown via Hamiltonian connections +* ``has_ibm_solver`` — whether the method can call IBM ``solve_fermion`` +* ``has_pt2_selection`` — whether PT2-based config selection is supported +* ``supports_initial_basis`` — whether a warm-start ``initial_basis`` tensor is accepted +* ``description`` — a short human-readable summary +* ``pipeline_folder`` — the 3-digit experiment folder that wraps this method + +The 010-013 wrapper scripts use this registry so they can dispatch by id +without hard-importing each method module, and downstream tooling (benchmark +harnesses, configuration validators, hyperparameter sweep runners) can do +the same. + +.. code-block:: python + + from qvartools.methods.nqs import METHODS_REGISTRY + from qvartools.molecules import get_molecule + + info = METHODS_REGISTRY["hi_nqs_sqd"] + config = info["config_cls"](n_iterations=5, device="cuda") + + hamiltonian, mol_info = get_molecule("H2") + result = info["run_fn"](hamiltonian, mol_info, config=config) + print(f"Energy: {result.energy:.10f} Ha") + +Numbering convention +-------------------- + +The 3-digit prefix scheme leaves room for catalog growth: + +* **001-009** — Ablation pipeline groups (current) +* **010-099** — Method-as-pipeline catalog (4 used: 010-013, 86 free) +* **100-199** — Cross-method sweeps (e.g., ``100_h2_all_methods``) +* **200+** — Hyperparameter sweeps + The FlowGuidedKrylovPipeline ----------------------------- @@ -65,7 +149,7 @@ entropy regularization). flow and apply diversity-aware selection to ensure representation across excitation ranks. -**Stage 2.5: Expand** (Groups 03, 08 only) — Enlarge the basis via CIPSI-style +**Stage 2.5: Expand** (Groups 003, 008 only) — Enlarge the basis via CIPSI-style perturbative selection using Hamiltonian connections. **Stage 3: Diagonalize** — Run Classical Krylov (SKQD), Quantum Circuit Krylov, @@ -97,45 +181,59 @@ or SQD (batch diag) to compute the ground-state energy. Iterative Pipelines -------------------- -Groups 06-08 use an iterative loop where the ground-state eigenvector -from each diagonalization is fed back as a training target for the next NQS -iteration: +Groups 006-008 (and method-as-pipeline entries 010-011) use an iterative loop +where the ground-state eigenvector from each diagonalization is fed back as a +training target for the next NQS iteration. The cleanest way to call an +iterative method programmatically is via ``METHODS_REGISTRY``: .. code-block:: python - from qvartools.methods.nqs.hi_nqs_skqd import HINQSSKQDConfig, run_hi_nqs_skqd + from qvartools.methods.nqs import METHODS_REGISTRY from qvartools.molecules import get_molecule hamiltonian, mol_info = get_molecule("H2") - mol_info["n_orbitals"] = hamiltonian.integrals.n_orbitals - mol_info["n_alpha"] = hamiltonian.integrals.n_alpha - mol_info["n_beta"] = hamiltonian.integrals.n_beta + # mol_info from get_molecule() omits orbital counts; every runner extracts + # them from hamiltonian.integrals automatically via extract_orbital_counts. - config = HINQSSKQDConfig( + info = METHODS_REGISTRY["hi_nqs_skqd"] + config = info["config_cls"]( n_iterations=10, n_samples_per_iter=5000, device="cuda", ) - result = run_hi_nqs_skqd(hamiltonian, mol_info, config=config) + result = info["run_fn"](hamiltonian, mol_info, config=config) print(f"Energy: {result.energy:.10f} Ha") print(f"Converged: {result.converged}") +Direct imports still work if you prefer explicit symbols +(``from qvartools.methods.nqs import HINQSSKQDConfig, run_hi_nqs_skqd``), but +the registry-based pattern is what the 010-013 pipeline wrappers use and is +more robust to future additions. + Running Experiment Scripts -------------------------- -All 24 pipelines live in ``experiments/pipelines/``: +All 33 pipelines live in ``experiments/pipelines/``: .. code-block:: bash - # Run a single pipeline - python experiments/pipelines/01_dci/dci_krylov_classical.py h2 --device cuda + # Run a single ablation pipeline + python experiments/pipelines/001_dci/dci_krylov_classical.py h2 --device cuda - # Run all 24 pipelines and compare + # Run a method-as-pipeline benchmark + python experiments/pipelines/010_hi_nqs_sqd/default.py h2 --device cuda + python experiments/pipelines/010_hi_nqs_sqd/pt2.py h2 --device cuda + python experiments/pipelines/013_nqs_skqd/default.py h2 --device cpu + + # Run all 33 pipelines and compare python experiments/pipelines/run_all_pipelines.py h2 --device cuda + # Filter by group prefix (3-digit) + python experiments/pipelines/run_all_pipelines.py h2 --only 001 005 010 + # Run with a YAML config - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py lih \ - --config experiments/pipelines/configs/02_nf_dci.yaml --max-epochs 200 + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py lih \ + --config experiments/pipelines/configs/002_nf_dci.yaml --max-epochs 200 See :doc:`yaml_configs` for details on the configuration system. diff --git a/docs/source/user_guide/yaml_configs.rst b/docs/source/user_guide/yaml_configs.rst index b6a60e1..ded334b 100644 --- a/docs/source/user_guide/yaml_configs.rst +++ b/docs/source/user_guide/yaml_configs.rst @@ -18,8 +18,8 @@ Each pipeline group has a matching YAML config in .. code-block:: bash - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py \ - --config experiments/pipelines/configs/02_nf_dci.yaml + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py \ + --config experiments/pipelines/configs/002_nf_dci.yaml CLI Overrides ------------- @@ -29,36 +29,57 @@ Any parameter can be overridden on the command line: .. code-block:: bash # Use YAML config but override the molecule and max epochs - python experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py lih \ - --config experiments/pipelines/configs/02_nf_dci.yaml \ + python experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py lih \ + --config experiments/pipelines/configs/002_nf_dci.yaml \ --max-epochs 200 \ --teacher-weight 0.6 Available Config Files ---------------------- +Tier 1 — Ablation groups (one flat YAML per group): + .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 38 62 * - File - Pipeline Group - * - ``01_dci.yaml`` + * - ``001_dci.yaml`` - Direct-CI (HF+S+D) — no NF training - * - ``02_nf_dci.yaml`` + * - ``002_nf_dci.yaml`` - NF-trained + Direct-CI merged basis - * - ``03_nf_dci_pt2.yaml`` + * - ``003_nf_dci_pt2.yaml`` - NF + DCI + PT2 perturbative expansion - * - ``04_nf_only.yaml`` + * - ``004_nf_only.yaml`` - NF-only basis (ablation, no DCI merge) - * - ``05_hf_only.yaml`` + * - ``005_hf_only.yaml`` - HF-only reference state (baseline) - * - ``06_iterative_nqs.yaml`` + * - ``006_iterative_nqs.yaml`` - Iterative NQS sampling + diag - * - ``07_iterative_nqs_dci.yaml`` + * - ``007_iterative_nqs_dci.yaml`` - NF+DCI merge then iterative NQS - * - ``08_iterative_nqs_dci_pt2.yaml`` + * - ``008_iterative_nqs_dci_pt2.yaml`` - NF+DCI+PT2 then iterative NQS + * - ``009_vqe.yaml`` + - CUDA-QX VQE (UCCSD + ADAPT-VQE) + +Tier 2 — Method-as-pipeline catalog (one multi-section YAML per method): + +.. list-table:: + :header-rows: 1 + :widths: 38 62 + + * - File + - Method (sections inside) + * - ``010_hi_nqs_sqd.yaml`` + - HI+NQS+SQD — sections: ``default``, ``pt2``, ``ibm_off`` + * - ``011_hi_nqs_skqd.yaml`` + - HI+NQS+SKQD — sections: ``default``, ``ibm_on`` + * - ``012_nqs_sqd.yaml`` + - NQS+SQD — section: ``default`` + * - ``013_nqs_skqd.yaml`` + - NQS+SKQD — section: ``default`` Config File Structure --------------------- diff --git a/experiments/config_loader.py b/experiments/config_loader.py index bcd89cf..e8cb6e5 100644 --- a/experiments/config_loader.py +++ b/experiments/config_loader.py @@ -104,7 +104,7 @@ def load_config( *config* is a flat dictionary containing every resolved parameter. """ args = parser.parse_args() - explicitly_provided = _get_explicit_cli_args(parser) + explicitly_provided = get_explicit_cli_args(parser) logger.debug("Explicitly provided CLI args: %s", explicitly_provided) # Start with built-in defaults @@ -163,12 +163,16 @@ def _load_yaml(path: str) -> dict[str, Any]: return dict(data) -def _get_explicit_cli_args(parser: argparse.ArgumentParser) -> set[str]: +def get_explicit_cli_args(parser: argparse.ArgumentParser) -> set[str]: """Return the set of argument *dest* names explicitly provided on the CLI. We parse ``sys.argv`` a second time using a sentinel default so that we can distinguish "user typed ``--device cpu``" from "argparse filled in the default". + + This is the preferred public entry point. The leading-underscore + alias :func:`_get_explicit_cli_args` is kept for backward compatibility + with existing callers (notably :mod:`tests.test_config_loader`). """ sentinel = object() probe = argparse.ArgumentParser(add_help=False) @@ -208,3 +212,9 @@ def _get_explicit_cli_args(parser: argparse.ArgumentParser) -> set[str]: probe_ns, _ = probe.parse_known_args() return {key for key, value in vars(probe_ns).items() if value is not sentinel} + + +# Backward-compat alias for callers (notably tests.test_config_loader) that +# imported the leading-underscore name before it became public. Prefer the +# public name `get_explicit_cli_args` in new code. +_get_explicit_cli_args = get_explicit_cli_args diff --git a/experiments/pipelines/01_dci/dci_krylov_classical.py b/experiments/pipelines/001_dci/dci_krylov_classical.py similarity index 100% rename from experiments/pipelines/01_dci/dci_krylov_classical.py rename to experiments/pipelines/001_dci/dci_krylov_classical.py diff --git a/experiments/pipelines/01_dci/dci_krylov_quantum.py b/experiments/pipelines/001_dci/dci_krylov_quantum.py similarity index 100% rename from experiments/pipelines/01_dci/dci_krylov_quantum.py rename to experiments/pipelines/001_dci/dci_krylov_quantum.py diff --git a/experiments/pipelines/01_dci/dci_sqd.py b/experiments/pipelines/001_dci/dci_sqd.py similarity index 100% rename from experiments/pipelines/01_dci/dci_sqd.py rename to experiments/pipelines/001_dci/dci_sqd.py diff --git a/experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py b/experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py similarity index 100% rename from experiments/pipelines/02_nf_dci/nf_dci_krylov_classical.py rename to experiments/pipelines/002_nf_dci/nf_dci_krylov_classical.py diff --git a/experiments/pipelines/02_nf_dci/nf_dci_krylov_quantum.py b/experiments/pipelines/002_nf_dci/nf_dci_krylov_quantum.py similarity index 100% rename from experiments/pipelines/02_nf_dci/nf_dci_krylov_quantum.py rename to experiments/pipelines/002_nf_dci/nf_dci_krylov_quantum.py diff --git a/experiments/pipelines/02_nf_dci/nf_dci_sqd.py b/experiments/pipelines/002_nf_dci/nf_dci_sqd.py similarity index 100% rename from experiments/pipelines/02_nf_dci/nf_dci_sqd.py rename to experiments/pipelines/002_nf_dci/nf_dci_sqd.py diff --git a/experiments/pipelines/03_nf_dci_pt2/nf_dci_pt2_krylov_classical.py b/experiments/pipelines/003_nf_dci_pt2/nf_dci_pt2_krylov_classical.py similarity index 100% rename from experiments/pipelines/03_nf_dci_pt2/nf_dci_pt2_krylov_classical.py rename to experiments/pipelines/003_nf_dci_pt2/nf_dci_pt2_krylov_classical.py diff --git a/experiments/pipelines/03_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py b/experiments/pipelines/003_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py similarity index 100% rename from experiments/pipelines/03_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py rename to experiments/pipelines/003_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py diff --git a/experiments/pipelines/03_nf_dci_pt2/nf_dci_pt2_sqd.py b/experiments/pipelines/003_nf_dci_pt2/nf_dci_pt2_sqd.py similarity index 100% rename from experiments/pipelines/03_nf_dci_pt2/nf_dci_pt2_sqd.py rename to experiments/pipelines/003_nf_dci_pt2/nf_dci_pt2_sqd.py diff --git a/experiments/pipelines/04_nf_only/nf_krylov_classical.py b/experiments/pipelines/004_nf_only/nf_krylov_classical.py similarity index 100% rename from experiments/pipelines/04_nf_only/nf_krylov_classical.py rename to experiments/pipelines/004_nf_only/nf_krylov_classical.py diff --git a/experiments/pipelines/04_nf_only/nf_krylov_quantum.py b/experiments/pipelines/004_nf_only/nf_krylov_quantum.py similarity index 100% rename from experiments/pipelines/04_nf_only/nf_krylov_quantum.py rename to experiments/pipelines/004_nf_only/nf_krylov_quantum.py diff --git a/experiments/pipelines/04_nf_only/nf_sqd.py b/experiments/pipelines/004_nf_only/nf_sqd.py similarity index 100% rename from experiments/pipelines/04_nf_only/nf_sqd.py rename to experiments/pipelines/004_nf_only/nf_sqd.py diff --git a/experiments/pipelines/05_hf_only/hf_krylov_classical.py b/experiments/pipelines/005_hf_only/hf_krylov_classical.py similarity index 100% rename from experiments/pipelines/05_hf_only/hf_krylov_classical.py rename to experiments/pipelines/005_hf_only/hf_krylov_classical.py diff --git a/experiments/pipelines/05_hf_only/hf_krylov_quantum.py b/experiments/pipelines/005_hf_only/hf_krylov_quantum.py similarity index 100% rename from experiments/pipelines/05_hf_only/hf_krylov_quantum.py rename to experiments/pipelines/005_hf_only/hf_krylov_quantum.py diff --git a/experiments/pipelines/05_hf_only/hf_sqd.py b/experiments/pipelines/005_hf_only/hf_sqd.py similarity index 100% rename from experiments/pipelines/05_hf_only/hf_sqd.py rename to experiments/pipelines/005_hf_only/hf_sqd.py diff --git a/experiments/pipelines/06_iterative_nqs/iter_nqs_krylov_classical.py b/experiments/pipelines/006_iterative_nqs/iter_nqs_krylov_classical.py similarity index 100% rename from experiments/pipelines/06_iterative_nqs/iter_nqs_krylov_classical.py rename to experiments/pipelines/006_iterative_nqs/iter_nqs_krylov_classical.py diff --git a/experiments/pipelines/06_iterative_nqs/iter_nqs_krylov_quantum.py b/experiments/pipelines/006_iterative_nqs/iter_nqs_krylov_quantum.py similarity index 100% rename from experiments/pipelines/06_iterative_nqs/iter_nqs_krylov_quantum.py rename to experiments/pipelines/006_iterative_nqs/iter_nqs_krylov_quantum.py diff --git a/experiments/pipelines/06_iterative_nqs/iter_nqs_sqd.py b/experiments/pipelines/006_iterative_nqs/iter_nqs_sqd.py similarity index 100% rename from experiments/pipelines/06_iterative_nqs/iter_nqs_sqd.py rename to experiments/pipelines/006_iterative_nqs/iter_nqs_sqd.py diff --git a/experiments/pipelines/07_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py b/experiments/pipelines/007_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py similarity index 100% rename from experiments/pipelines/07_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py rename to experiments/pipelines/007_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py diff --git a/experiments/pipelines/07_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py b/experiments/pipelines/007_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py similarity index 100% rename from experiments/pipelines/07_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py rename to experiments/pipelines/007_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py diff --git a/experiments/pipelines/07_iterative_nqs_dci/iter_nqs_dci_sqd.py b/experiments/pipelines/007_iterative_nqs_dci/iter_nqs_dci_sqd.py similarity index 100% rename from experiments/pipelines/07_iterative_nqs_dci/iter_nqs_dci_sqd.py rename to experiments/pipelines/007_iterative_nqs_dci/iter_nqs_dci_sqd.py diff --git a/experiments/pipelines/08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py b/experiments/pipelines/008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py similarity index 100% rename from experiments/pipelines/08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py rename to experiments/pipelines/008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py diff --git a/experiments/pipelines/08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py b/experiments/pipelines/008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py similarity index 100% rename from experiments/pipelines/08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py rename to experiments/pipelines/008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py diff --git a/experiments/pipelines/08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py b/experiments/pipelines/008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py similarity index 100% rename from experiments/pipelines/08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py rename to experiments/pipelines/008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py diff --git a/experiments/pipelines/09_vqe/vqe_adapt.py b/experiments/pipelines/009_vqe/vqe_adapt.py similarity index 100% rename from experiments/pipelines/09_vqe/vqe_adapt.py rename to experiments/pipelines/009_vqe/vqe_adapt.py diff --git a/experiments/pipelines/09_vqe/vqe_uccsd.py b/experiments/pipelines/009_vqe/vqe_uccsd.py similarity index 100% rename from experiments/pipelines/09_vqe/vqe_uccsd.py rename to experiments/pipelines/009_vqe/vqe_uccsd.py diff --git a/experiments/pipelines/010_hi_nqs_sqd/default.py b/experiments/pipelines/010_hi_nqs_sqd/default.py new file mode 100644 index 0000000..a27d437 --- /dev/null +++ b/experiments/pipelines/010_hi_nqs_sqd/default.py @@ -0,0 +1,149 @@ +"""Pipeline 010 hi_nqs_sqd / default — baseline HI+NQS+SQD (auto IBM detect). + +Wraps :func:`qvartools.methods.nqs.run_hi_nqs_sqd` via ``METHODS_REGISTRY`` with +the ``[default]`` section of ``configs/010_hi_nqs_sqd.yaml``. + +Usage:: + + python experiments/pipelines/010_hi_nqs_sqd/default.py h2 --device cuda + python experiments/pipelines/010_hi_nqs_sqd/default.py h2 \ + --config experiments/pipelines/configs/010_hi_nqs_sqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "hi_nqs_sqd" +VARIANT = "default" +PIPELINE_ID = "010" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): baseline HI+NQS+SQD (auto IBM detect)." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/010_hi_nqs_sqd/ibm_off.py b/experiments/pipelines/010_hi_nqs_sqd/ibm_off.py new file mode 100644 index 0000000..73c8261 --- /dev/null +++ b/experiments/pipelines/010_hi_nqs_sqd/ibm_off.py @@ -0,0 +1,149 @@ +"""Pipeline 010 hi_nqs_sqd / ibm_off — force GPU fallback (no IBM solve_fermion). + +Wraps :func:`qvartools.methods.nqs.run_hi_nqs_sqd` via ``METHODS_REGISTRY`` with +the ``[ibm_off]`` section of ``configs/010_hi_nqs_sqd.yaml``. + +Usage:: + + python experiments/pipelines/010_hi_nqs_sqd/ibm_off.py h2 --device cuda + python experiments/pipelines/010_hi_nqs_sqd/ibm_off.py h2 \ + --config experiments/pipelines/configs/010_hi_nqs_sqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "hi_nqs_sqd" +VARIANT = "ibm_off" +PIPELINE_ID = "010" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): force GPU fallback (no IBM solve_fermion)." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/010_hi_nqs_sqd/pt2.py b/experiments/pipelines/010_hi_nqs_sqd/pt2.py new file mode 100644 index 0000000..911b1f8 --- /dev/null +++ b/experiments/pipelines/010_hi_nqs_sqd/pt2.py @@ -0,0 +1,149 @@ +"""Pipeline 010 hi_nqs_sqd / pt2 — PT2 selection + temperature annealing. + +Wraps :func:`qvartools.methods.nqs.run_hi_nqs_sqd` via ``METHODS_REGISTRY`` with +the ``[pt2]`` section of ``configs/010_hi_nqs_sqd.yaml``. + +Usage:: + + python experiments/pipelines/010_hi_nqs_sqd/pt2.py h2 --device cuda + python experiments/pipelines/010_hi_nqs_sqd/pt2.py h2 \ + --config experiments/pipelines/configs/010_hi_nqs_sqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "hi_nqs_sqd" +VARIANT = "pt2" +PIPELINE_ID = "010" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): PT2 selection + temperature annealing." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/011_hi_nqs_skqd/default.py b/experiments/pipelines/011_hi_nqs_skqd/default.py new file mode 100644 index 0000000..2279202 --- /dev/null +++ b/experiments/pipelines/011_hi_nqs_skqd/default.py @@ -0,0 +1,149 @@ +"""Pipeline 011 hi_nqs_skqd / default — baseline HI+NQS+SKQD with Krylov expansion. + +Wraps :func:`qvartools.methods.nqs.run_hi_nqs_skqd` via ``METHODS_REGISTRY`` with +the ``[default]`` section of ``configs/011_hi_nqs_skqd.yaml``. + +Usage:: + + python experiments/pipelines/011_hi_nqs_skqd/default.py h2 --device cuda + python experiments/pipelines/011_hi_nqs_skqd/default.py h2 \ + --config experiments/pipelines/configs/011_hi_nqs_skqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "hi_nqs_skqd" +VARIANT = "default" +PIPELINE_ID = "011" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): baseline HI+NQS+SKQD with Krylov expansion." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/011_hi_nqs_skqd/ibm_on.py b/experiments/pipelines/011_hi_nqs_skqd/ibm_on.py new file mode 100644 index 0000000..d56c2d9 --- /dev/null +++ b/experiments/pipelines/011_hi_nqs_skqd/ibm_on.py @@ -0,0 +1,149 @@ +"""Pipeline 011 hi_nqs_skqd / ibm_on — enable IBM solver + S-CORE recovery. + +Wraps :func:`qvartools.methods.nqs.run_hi_nqs_skqd` via ``METHODS_REGISTRY`` with +the ``[ibm_on]`` section of ``configs/011_hi_nqs_skqd.yaml``. + +Usage:: + + python experiments/pipelines/011_hi_nqs_skqd/ibm_on.py h2 --device cuda + python experiments/pipelines/011_hi_nqs_skqd/ibm_on.py h2 \ + --config experiments/pipelines/configs/011_hi_nqs_skqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "hi_nqs_skqd" +VARIANT = "ibm_on" +PIPELINE_ID = "011" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): enable IBM solver + S-CORE recovery." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/012_nqs_sqd/default.py b/experiments/pipelines/012_nqs_sqd/default.py new file mode 100644 index 0000000..db421c7 --- /dev/null +++ b/experiments/pipelines/012_nqs_sqd/default.py @@ -0,0 +1,149 @@ +"""Pipeline 012 nqs_sqd / default — two-stage NQS train + SQD diag (no iteration). + +Wraps :func:`qvartools.methods.nqs.run_nqs_sqd` via ``METHODS_REGISTRY`` with +the ``[default]`` section of ``configs/012_nqs_sqd.yaml``. + +Usage:: + + python experiments/pipelines/012_nqs_sqd/default.py h2 --device cuda + python experiments/pipelines/012_nqs_sqd/default.py h2 \ + --config experiments/pipelines/configs/012_nqs_sqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "nqs_sqd" +VARIANT = "default" +PIPELINE_ID = "012" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): two-stage NQS train + SQD diag (no iteration)." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/013_nqs_skqd/default.py b/experiments/pipelines/013_nqs_skqd/default.py new file mode 100644 index 0000000..2f3f242 --- /dev/null +++ b/experiments/pipelines/013_nqs_skqd/default.py @@ -0,0 +1,149 @@ +"""Pipeline 013 nqs_skqd / default — two-stage NQS train + Krylov expand + diag. + +Wraps :func:`qvartools.methods.nqs.run_nqs_skqd` via ``METHODS_REGISTRY`` with +the ``[default]`` section of ``configs/013_nqs_skqd.yaml``. + +Usage:: + + python experiments/pipelines/013_nqs_skqd/default.py h2 --device cuda + python experiments/pipelines/013_nqs_skqd/default.py h2 \ + --config experiments/pipelines/configs/013_nqs_skqd.yaml +""" + +from __future__ import annotations + +import logging +import sys +import time +from pathlib import Path + +# Make config_loader importable when run as a script +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) +import torch # noqa: E402 +from config_loader import ( # noqa: E402 + create_base_parser, + get_explicit_cli_args, + load_config, +) + +from qvartools.methods.nqs import METHODS_REGISTRY # noqa: E402 +from qvartools.molecules import get_molecule # noqa: E402 +from qvartools.solvers import FCISolver # noqa: E402 + +METHOD_KEY = "nqs_skqd" +VARIANT = "default" +PIPELINE_ID = "013" + +# Reserved top-level keys handled outside the dataclass — filtered out +# before checking for unknown YAML keys so they don't produce false warnings. +_RESERVED_CFG_KEYS = {"molecule", "device", "config", "verbose"} + + +def _resolve_section(cfg: dict, variant: str) -> dict: + """Pick the requested variant section from a multi-section YAML. + + Falls back to the flat cfg if the variant section is absent, but emits + a warning when the YAML looks multi-section (has other dict values) so + silent misreads don't hide stale configs. + """ + section_value = cfg.get(variant) + if isinstance(section_value, dict): + return section_value + other_sections = [k for k, v in cfg.items() if isinstance(v, dict) and k != variant] + if other_sections: + print( + f"WARNING: --config YAML has sections {sorted(other_sections)} but " + f"this script expected section '{variant}'. Falling back to " + f"flat cfg; most keys may be silently dropped and the method will " + f"run with dataclass defaults.", + file=sys.stderr, + ) + return cfg + + +def _build_config_kwargs(section: dict, config_cls, device: str) -> dict: + """Filter a YAML section to the valid dataclass fields, warning on unknown keys.""" + valid_keys = set(config_cls.__dataclass_fields__.keys()) + section_scalar_keys = {k for k, v in section.items() if not isinstance(v, dict)} + unknown = section_scalar_keys - valid_keys - _RESERVED_CFG_KEYS + if unknown: + print( + f"WARNING: YAML section has keys that are not fields of " + f"{config_cls.__name__} and will be silently ignored: " + f"{sorted(unknown)}", + file=sys.stderr, + ) + config_kwargs = {k: v for k, v in section.items() if k in valid_keys} + config_kwargs["device"] = device + return config_kwargs + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + ) + + parser = create_base_parser( + f"Pipeline {PIPELINE_ID} {METHOD_KEY} ({VARIANT}): two-stage NQS train + Krylov expand + diag." + ) + args, cfg = load_config(parser) + # Detect which CLI args the user actually typed (vs merged defaults). + # Needed because load_config mutates args with YAML/_DEFAULTS values. + explicit_cli = get_explicit_cli_args(parser) + + section = _resolve_section(cfg, VARIANT) + + # Device precedence: explicit CLI > section > "auto" + if "device" in explicit_cli: + device = args.device + else: + device = section.get("device", "auto") + if device == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Molecule precedence: explicit CLI positional > section > "h2" + if "molecule" in explicit_cli: + molecule = args.molecule + else: + molecule = section.get("molecule", "h2") + + # Dispatch via registry (no hard imports of specific config/runner) + method_info = METHODS_REGISTRY[METHOD_KEY] + config_cls = method_info["config_cls"] + run_fn = method_info["run_fn"] + + hamiltonian, mol_info = get_molecule(molecule, device=device) + n_qubits = mol_info["n_qubits"] + print(f"Molecule : {molecule}") + print(f"Qubits : {n_qubits}") + print(f"Pipeline : {PIPELINE_ID}_{METHOD_KEY}/{VARIANT}") + print(f"Device : {device}") + print("=" * 60) + + fci = FCISolver().solve(hamiltonian, mol_info) + if fci.energy is not None: + print(f"Exact (FCI) energy: {fci.energy:.10f} Ha") + print("-" * 60) + + config_kwargs = _build_config_kwargs(section, config_cls, device) + config = config_cls(**config_kwargs) + + t_start = time.perf_counter() + result = run_fn(hamiltonian, mol_info, config=config) + wall_time = time.perf_counter() - t_start + + err_mha = (result.energy - fci.energy) * 1000.0 if fci.energy is not None else None + print("\n" + "=" * 60) + print(f"PIPELINE {PIPELINE_ID}_{METHOD_KEY} ({VARIANT}) RESULTS") + print("=" * 60) + print(f"Best energy : {result.energy:.10f} Ha") + print(f"Final energy: {result.energy:.10f} Ha") + if err_mha is not None: + print(f"Error : {err_mha:.4f} mHa") + print(f"Wall time : {wall_time:.2f} s") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/experiments/pipelines/configs/01_dci.yaml b/experiments/pipelines/configs/001_dci.yaml similarity index 87% rename from experiments/pipelines/configs/01_dci.yaml rename to experiments/pipelines/configs/001_dci.yaml index 94795f9..6538604 100644 --- a/experiments/pipelines/configs/01_dci.yaml +++ b/experiments/pipelines/configs/001_dci.yaml @@ -1,4 +1,4 @@ -# 01_dci: Direct-CI (HF + singles + doubles) pipelines +# 001_dci: Direct-CI (HF + singles + doubles) pipelines # No NF training. Basis generated deterministically. molecule: h2 diff --git a/experiments/pipelines/configs/02_nf_dci.yaml b/experiments/pipelines/configs/002_nf_dci.yaml similarity index 89% rename from experiments/pipelines/configs/02_nf_dci.yaml rename to experiments/pipelines/configs/002_nf_dci.yaml index 5dfdb19..ca2b549 100644 --- a/experiments/pipelines/configs/02_nf_dci.yaml +++ b/experiments/pipelines/configs/002_nf_dci.yaml @@ -1,4 +1,4 @@ -# 02_nf_dci: NF training + Direct-CI merge pipelines +# 002_nf_dci: NF training + Direct-CI merge pipelines molecule: h2 diff --git a/experiments/pipelines/configs/03_nf_dci_pt2.yaml b/experiments/pipelines/configs/003_nf_dci_pt2.yaml similarity index 87% rename from experiments/pipelines/configs/03_nf_dci_pt2.yaml rename to experiments/pipelines/configs/003_nf_dci_pt2.yaml index cf3a200..4fd1992 100644 --- a/experiments/pipelines/configs/03_nf_dci_pt2.yaml +++ b/experiments/pipelines/configs/003_nf_dci_pt2.yaml @@ -1,4 +1,4 @@ -# 03_nf_dci_pt2: NF training + Direct-CI merge + PT2 expansion pipelines +# 003_nf_dci_pt2: NF training + Direct-CI merge + PT2 expansion pipelines molecule: h2 diff --git a/experiments/pipelines/configs/04_nf_only.yaml b/experiments/pipelines/configs/004_nf_only.yaml similarity index 88% rename from experiments/pipelines/configs/04_nf_only.yaml rename to experiments/pipelines/configs/004_nf_only.yaml index e04526b..9915e59 100644 --- a/experiments/pipelines/configs/04_nf_only.yaml +++ b/experiments/pipelines/configs/004_nf_only.yaml @@ -1,4 +1,4 @@ -# 04_nf_only: NF-only pipelines (ablation, no DCI merge) +# 004_nf_only: NF-only pipelines (ablation, no DCI merge) molecule: h2 diff --git a/experiments/pipelines/configs/05_hf_only.yaml b/experiments/pipelines/configs/005_hf_only.yaml similarity index 83% rename from experiments/pipelines/configs/05_hf_only.yaml rename to experiments/pipelines/configs/005_hf_only.yaml index 4678adc..07c05a6 100644 --- a/experiments/pipelines/configs/05_hf_only.yaml +++ b/experiments/pipelines/configs/005_hf_only.yaml @@ -1,4 +1,4 @@ -# 05_hf_only: HF-only reference state pipelines (baseline) +# 005_hf_only: HF-only reference state pipelines (baseline) molecule: h2 diff --git a/experiments/pipelines/configs/06_iterative_nqs.yaml b/experiments/pipelines/configs/006_iterative_nqs.yaml similarity index 82% rename from experiments/pipelines/configs/06_iterative_nqs.yaml rename to experiments/pipelines/configs/006_iterative_nqs.yaml index a43b5d2..acbe795 100644 --- a/experiments/pipelines/configs/06_iterative_nqs.yaml +++ b/experiments/pipelines/configs/006_iterative_nqs.yaml @@ -1,4 +1,4 @@ -# 06_iterative_nqs: Iterative NQS with eigenvector feedback pipelines +# 006_iterative_nqs: Iterative NQS with eigenvector feedback pipelines molecule: h2 diff --git a/experiments/pipelines/configs/07_iterative_nqs_dci.yaml b/experiments/pipelines/configs/007_iterative_nqs_dci.yaml similarity index 83% rename from experiments/pipelines/configs/07_iterative_nqs_dci.yaml rename to experiments/pipelines/configs/007_iterative_nqs_dci.yaml index e552b72..da9c60b 100644 --- a/experiments/pipelines/configs/07_iterative_nqs_dci.yaml +++ b/experiments/pipelines/configs/007_iterative_nqs_dci.yaml @@ -1,4 +1,4 @@ -# 07_iterative_nqs_dci: Iterative NQS + DCI seed pipelines +# 007_iterative_nqs_dci: Iterative NQS + DCI seed pipelines molecule: h2 diff --git a/experiments/pipelines/configs/08_iterative_nqs_dci_pt2.yaml b/experiments/pipelines/configs/008_iterative_nqs_dci_pt2.yaml similarity index 81% rename from experiments/pipelines/configs/08_iterative_nqs_dci_pt2.yaml rename to experiments/pipelines/configs/008_iterative_nqs_dci_pt2.yaml index 1607b14..05b42c9 100644 --- a/experiments/pipelines/configs/08_iterative_nqs_dci_pt2.yaml +++ b/experiments/pipelines/configs/008_iterative_nqs_dci_pt2.yaml @@ -1,4 +1,4 @@ -# 08_iterative_nqs_dci_pt2: Iterative NQS + DCI seed + PT2 expansion pipelines +# 008_iterative_nqs_dci_pt2: Iterative NQS + DCI seed + PT2 expansion pipelines molecule: h2 diff --git a/experiments/pipelines/configs/09_vqe.yaml b/experiments/pipelines/configs/009_vqe.yaml similarity index 67% rename from experiments/pipelines/configs/09_vqe.yaml rename to experiments/pipelines/configs/009_vqe.yaml index 78410e7..474e2fe 100644 --- a/experiments/pipelines/configs/09_vqe.yaml +++ b/experiments/pipelines/configs/009_vqe.yaml @@ -1,4 +1,4 @@ -# 09_vqe: CUDA-QX VQE pipelines +# 009_vqe: CUDA-QX VQE pipelines molecule: h2 optimizer: cobyla diff --git a/experiments/pipelines/configs/010_hi_nqs_sqd.yaml b/experiments/pipelines/configs/010_hi_nqs_sqd.yaml new file mode 100644 index 0000000..eaf1d9f --- /dev/null +++ b/experiments/pipelines/configs/010_hi_nqs_sqd.yaml @@ -0,0 +1,56 @@ +# 010_hi_nqs_sqd: HI+NQS+SQD method (iterative, optional PT2/IBM) +# +# Each top-level section corresponds to a script in 010_hi_nqs_sqd/. +# Defaults are tuned for H2 smoke testing — scale up for larger systems. + +default: + molecule: h2 + device: auto + n_iterations: 5 + n_samples_per_iter: 2000 + n_batches: 3 + max_configs_per_batch: 2000 + energy_tol: 1.0e-5 + nqs_lr: 1.0e-3 + nqs_train_epochs: 30 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + temperature: 1.0 + use_ibm_solver: null # auto-enable when qiskit_addon_sqd is installed + +pt2: + molecule: h2 + device: auto + n_iterations: 5 + n_samples_per_iter: 2000 + n_batches: 3 + max_configs_per_batch: 2000 + energy_tol: 1.0e-5 + nqs_lr: 1.0e-3 + nqs_train_epochs: 30 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + use_pt2_selection: true + pt2_top_k: 1000 + max_basis_size: 5000 + initial_temperature: 1.5 + final_temperature: 0.3 + use_ibm_solver: null + +ibm_off: + molecule: h2 + device: auto + n_iterations: 5 + n_samples_per_iter: 2000 + n_batches: 3 + max_configs_per_batch: 2000 + energy_tol: 1.0e-5 + nqs_lr: 1.0e-3 + nqs_train_epochs: 30 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + temperature: 1.0 + use_ibm_solver: false # force GPU fallback (no IBM solve_fermion) diff --git a/experiments/pipelines/configs/011_hi_nqs_skqd.yaml b/experiments/pipelines/configs/011_hi_nqs_skqd.yaml new file mode 100644 index 0000000..5c187fd --- /dev/null +++ b/experiments/pipelines/configs/011_hi_nqs_skqd.yaml @@ -0,0 +1,40 @@ +# 011_hi_nqs_skqd: HI+NQS+SKQD method (iterative + Krylov expansion) +# +# Each top-level section corresponds to a script in 011_hi_nqs_skqd/. +# Defaults are tuned for H2 smoke testing — scale up for larger systems. + +default: + molecule: h2 + device: auto + n_iterations: 5 + n_samples_per_iter: 2000 + n_batches: 3 + max_configs_per_batch: 2000 + energy_tol: 1.0e-5 + nqs_lr: 1.0e-3 + nqs_train_epochs: 30 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + temperature: 1.0 + krylov_max_new: 200 + krylov_n_ref: 10 + use_ibm_solver: false # baseline keeps IBM off + +ibm_on: + molecule: h2 + device: auto + n_iterations: 5 + n_samples_per_iter: 2000 + n_batches: 3 + max_configs_per_batch: 2000 + energy_tol: 1.0e-5 + nqs_lr: 1.0e-3 + nqs_train_epochs: 30 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + temperature: 1.0 + krylov_max_new: 200 + krylov_n_ref: 10 + use_ibm_solver: true # enable IBM solver + S-CORE recovery diff --git a/experiments/pipelines/configs/012_nqs_sqd.yaml b/experiments/pipelines/configs/012_nqs_sqd.yaml new file mode 100644 index 0000000..c4a9b75 --- /dev/null +++ b/experiments/pipelines/configs/012_nqs_sqd.yaml @@ -0,0 +1,15 @@ +# 012_nqs_sqd: NQS+SQD method (two-stage, no iteration) +# +# Single-section YAML. Defaults are tuned for H2 smoke testing. + +default: + molecule: h2 + device: auto + n_samples: 2000 + nqs_train_epochs: 100 + nqs_lr: 1.0e-3 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + temperature: 1.0 + max_basis_size: 5000 diff --git a/experiments/pipelines/configs/013_nqs_skqd.yaml b/experiments/pipelines/configs/013_nqs_skqd.yaml new file mode 100644 index 0000000..a1fb6f5 --- /dev/null +++ b/experiments/pipelines/configs/013_nqs_skqd.yaml @@ -0,0 +1,17 @@ +# 013_nqs_skqd: NQS+SKQD method (two-stage with Krylov expansion) +# +# Single-section YAML. Defaults are tuned for H2 smoke testing. + +default: + molecule: h2 + device: auto + n_samples: 2000 + nqs_train_epochs: 100 + nqs_lr: 1.0e-3 + embed_dim: 64 + n_heads: 4 + n_layers: 4 + temperature: 1.0 + krylov_max_new: 200 + krylov_n_ref: 10 + max_basis_size: 5000 diff --git a/experiments/pipelines/run_all_pipelines.py b/experiments/pipelines/run_all_pipelines.py index 2fbf16e..1528ac3 100644 --- a/experiments/pipelines/run_all_pipelines.py +++ b/experiments/pipelines/run_all_pipelines.py @@ -3,7 +3,7 @@ Usage: python run_all_pipelines.py h2 python run_all_pipelines.py lih --skip-quantum # skip CUDA-Q pipelines - python run_all_pipelines.py h2 --only 01 02 # run only groups 01 and 02 + python run_all_pipelines.py h2 --only 001 002 # run only groups 001 and 002 python run_all_pipelines.py h2 --skip-iterative # skip slow iterative pipelines """ @@ -29,136 +29,184 @@ PIPELINES = [ # (group, script_path, short_name, description) ( - "01_dci", - "01_dci/dci_krylov_classical.py", + "001_dci", + "001_dci/dci_krylov_classical.py", "DCI+Krylov-C", "DCI → Classical Krylov", ), - ("01_dci", "01_dci/dci_krylov_quantum.py", "DCI+Krylov-Q", "DCI → Quantum Krylov"), - ("01_dci", "01_dci/dci_sqd.py", "DCI+SQD", "DCI → SQD"), ( - "02_nf_dci", - "02_nf_dci/nf_dci_krylov_classical.py", + "001_dci", + "001_dci/dci_krylov_quantum.py", + "DCI+Krylov-Q", + "DCI → Quantum Krylov", + ), + ("001_dci", "001_dci/dci_sqd.py", "DCI+SQD", "DCI → SQD"), + ( + "002_nf_dci", + "002_nf_dci/nf_dci_krylov_classical.py", "NF+DCI+Krylov-C", "NF+DCI → Classical Krylov", ), ( - "02_nf_dci", - "02_nf_dci/nf_dci_krylov_quantum.py", + "002_nf_dci", + "002_nf_dci/nf_dci_krylov_quantum.py", "NF+DCI+Krylov-Q", "NF+DCI → Quantum Krylov", ), - ("02_nf_dci", "02_nf_dci/nf_dci_sqd.py", "NF+DCI+SQD", "NF+DCI → SQD"), + ("002_nf_dci", "002_nf_dci/nf_dci_sqd.py", "NF+DCI+SQD", "NF+DCI → SQD"), ( - "03_nf_dci_pt2", - "03_nf_dci_pt2/nf_dci_pt2_krylov_classical.py", + "003_nf_dci_pt2", + "003_nf_dci_pt2/nf_dci_pt2_krylov_classical.py", "NF+DCI+PT2+Krylov-C", "NF+DCI+PT2 → Classical Krylov", ), ( - "03_nf_dci_pt2", - "03_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py", + "003_nf_dci_pt2", + "003_nf_dci_pt2/nf_dci_pt2_krylov_quantum.py", "NF+DCI+PT2+Krylov-Q", "NF+DCI+PT2 → Quantum Krylov", ), ( - "03_nf_dci_pt2", - "03_nf_dci_pt2/nf_dci_pt2_sqd.py", + "003_nf_dci_pt2", + "003_nf_dci_pt2/nf_dci_pt2_sqd.py", "NF+DCI+PT2+SQD", "NF+DCI+PT2 → SQD", ), ( - "04_nf_only", - "04_nf_only/nf_krylov_classical.py", + "004_nf_only", + "004_nf_only/nf_krylov_classical.py", "NF+Krylov-C", "NF-only → Classical Krylov", ), ( - "04_nf_only", - "04_nf_only/nf_krylov_quantum.py", + "004_nf_only", + "004_nf_only/nf_krylov_quantum.py", "NF+Krylov-Q", "NF-only → Quantum Krylov", ), - ("04_nf_only", "04_nf_only/nf_sqd.py", "NF+SQD", "NF-only → SQD"), + ("004_nf_only", "004_nf_only/nf_sqd.py", "NF+SQD", "NF-only → SQD"), ( - "05_hf_only", - "05_hf_only/hf_krylov_classical.py", + "005_hf_only", + "005_hf_only/hf_krylov_classical.py", "HF+Krylov-C", "HF-only → Classical Krylov", ), ( - "05_hf_only", - "05_hf_only/hf_krylov_quantum.py", + "005_hf_only", + "005_hf_only/hf_krylov_quantum.py", "HF+Krylov-Q", "HF-only → Quantum Krylov", ), - ("05_hf_only", "05_hf_only/hf_sqd.py", "HF+SQD", "HF-only → SQD"), + ("005_hf_only", "005_hf_only/hf_sqd.py", "HF+SQD", "HF-only → SQD"), ( - "06_iterative_nqs", - "06_iterative_nqs/iter_nqs_krylov_classical.py", + "006_iterative_nqs", + "006_iterative_nqs/iter_nqs_krylov_classical.py", "Iter+Krylov-C", "Iterative NQS → Classical Krylov", ), ( - "06_iterative_nqs", - "06_iterative_nqs/iter_nqs_krylov_quantum.py", + "006_iterative_nqs", + "006_iterative_nqs/iter_nqs_krylov_quantum.py", "Iter+Krylov-Q", "Iterative NQS → Quantum Krylov", ), ( - "06_iterative_nqs", - "06_iterative_nqs/iter_nqs_sqd.py", + "006_iterative_nqs", + "006_iterative_nqs/iter_nqs_sqd.py", "Iter+SQD", "Iterative NQS → SQD", ), ( - "07_iterative_nqs_dci", - "07_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py", + "007_iterative_nqs_dci", + "007_iterative_nqs_dci/iter_nqs_dci_krylov_classical.py", "Iter+DCI+Krylov-C", "Iterative NQS+DCI → Classical Krylov", ), ( - "07_iterative_nqs_dci", - "07_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py", + "007_iterative_nqs_dci", + "007_iterative_nqs_dci/iter_nqs_dci_krylov_quantum.py", "Iter+DCI+Krylov-Q", "Iterative NQS+DCI → Quantum Krylov", ), ( - "07_iterative_nqs_dci", - "07_iterative_nqs_dci/iter_nqs_dci_sqd.py", + "007_iterative_nqs_dci", + "007_iterative_nqs_dci/iter_nqs_dci_sqd.py", "Iter+DCI+SQD", "Iterative NQS+DCI → SQD", ), ( - "08_iterative_nqs_dci_pt2", - "08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py", + "008_iterative_nqs_dci_pt2", + "008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_classical.py", "Iter+DCI+PT2+Krylov-C", "Iterative NQS+DCI+PT2 → Classical Krylov", ), ( - "08_iterative_nqs_dci_pt2", - "08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py", + "008_iterative_nqs_dci_pt2", + "008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_krylov_quantum.py", "Iter+DCI+PT2+Krylov-Q", "Iterative NQS+DCI+PT2 → Quantum Krylov", ), ( - "08_iterative_nqs_dci_pt2", - "08_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py", + "008_iterative_nqs_dci_pt2", + "008_iterative_nqs_dci_pt2/iter_nqs_dci_pt2_sqd.py", "Iter+DCI+PT2+SQD", "Iterative NQS+DCI+PT2 → SQD", ), ( - "09_vqe", - "09_vqe/vqe_uccsd.py", + "009_vqe", + "009_vqe/vqe_uccsd.py", "VQE-UCCSD", "CUDA-QX VQE with UCCSD ansatz", ), ( - "09_vqe", - "09_vqe/vqe_adapt.py", + "009_vqe", + "009_vqe/vqe_adapt.py", "ADAPT-VQE", "CUDA-QX ADAPT-VQE with GSD operator pool", ), + # ---- Method-as-pipeline catalog (010-099) ---- + ( + "010_hi_nqs_sqd", + "010_hi_nqs_sqd/default.py", + "HI-NQS-SQD/default", + "HI+NQS+SQD baseline (auto IBM detect)", + ), + ( + "010_hi_nqs_sqd", + "010_hi_nqs_sqd/pt2.py", + "HI-NQS-SQD/pt2", + "HI+NQS+SQD with PT2 selection + temperature annealing", + ), + ( + "010_hi_nqs_sqd", + "010_hi_nqs_sqd/ibm_off.py", + "HI-NQS-SQD/ibm_off", + "HI+NQS+SQD forced GPU fallback (no IBM)", + ), + ( + "011_hi_nqs_skqd", + "011_hi_nqs_skqd/default.py", + "HI-NQS-SKQD/default", + "HI+NQS+SKQD baseline with Krylov expansion", + ), + ( + "011_hi_nqs_skqd", + "011_hi_nqs_skqd/ibm_on.py", + "HI-NQS-SKQD/ibm_on", + "HI+NQS+SKQD with IBM solver + S-CORE", + ), + ( + "012_nqs_sqd", + "012_nqs_sqd/default.py", + "NQS-SQD/default", + "Two-stage NQS+SQD (no iteration)", + ), + ( + "013_nqs_skqd", + "013_nqs_skqd/default.py", + "NQS-SKQD/default", + "Two-stage NQS+SKQD with Krylov expansion", + ), ] @@ -276,7 +324,7 @@ def main() -> None: "--only", nargs="*", default=None, - help="Run only these groups (e.g., --only 01 02 04)", + help="Run only these groups (e.g., --only 001 002 004)", ) parser.add_argument( "--skip-quantum", @@ -316,6 +364,21 @@ def main() -> None: print(" FCI reference unavailable for this system.") print(f"{'=' * 80}\n") + # Detect the pre-2026-04-07 2-digit --only format and print a migration + # hint before silently filtering everything out. The catalog now uses + # 3-digit prefixes (001-013), so "01 02 04" matches no group. + if args.only: + legacy_2digit = [o for o in args.only if len(o) == 2 and o.isdigit()] + if legacy_2digit: + suggested = " ".join("0" + o for o in legacy_2digit) + print( + f"WARNING: --only values {legacy_2digit} use the old 2-digit " + f"format. Pipeline folders now use 3-digit prefixes (001-013). " + f"No groups will match these values. Use --only " + f"{suggested} instead.", + file=sys.stderr, + ) + # Filter pipelines pipelines_to_run = [] for group, script, name, desc in PIPELINES: @@ -323,9 +386,11 @@ def main() -> None: if args.only and group_num not in args.only: continue - if args.skip_quantum and ("Krylov-Q" in name or group == "09_vqe"): + if args.skip_quantum and ("Krylov-Q" in name or group == "009_vqe"): continue - if args.skip_iterative and group.startswith(("06", "07", "08")): + if args.skip_iterative and group.startswith( + ("006", "007", "008", "010", "011") + ): continue pipelines_to_run.append((group, script, name, desc)) diff --git a/src/qvartools/methods/nqs/__init__.py b/src/qvartools/methods/nqs/__init__.py index f62d5fb..285c1a9 100644 --- a/src/qvartools/methods/nqs/__init__.py +++ b/src/qvartools/methods/nqs/__init__.py @@ -1,7 +1,16 @@ -"""nqs --- NQS-based method pipelines.""" +"""nqs --- NQS-based method pipelines. + +Public entry points for the four NQS methods plus a ``METHODS_REGISTRY`` +that maps a stable method id to the runner, config class, and metadata. +The registry is used by the ``experiments/pipelines/010-013`` wrapper +scripts (and any future benchmark catalog tooling) so they can dispatch +by name without hard-importing each method module. +""" from __future__ import annotations +from typing import Any + from qvartools.methods.nqs.hi_nqs_skqd import HINQSSKQDConfig, run_hi_nqs_skqd from qvartools.methods.nqs.hi_nqs_sqd import HINQSSQDConfig, run_hi_nqs_sqd from qvartools.methods.nqs.nqs_skqd import NQSSKQDConfig, run_nqs_skqd @@ -16,4 +25,53 @@ "run_hi_nqs_sqd", "HINQSSKQDConfig", "run_hi_nqs_skqd", + "METHODS_REGISTRY", ] + + +METHODS_REGISTRY: dict[str, dict[str, Any]] = { + "nqs_sqd": { + "run_fn": run_nqs_sqd, + "config_cls": NQSSQDConfig, + "iterative": False, + "has_krylov_expansion": False, + "has_ibm_solver": False, + "has_pt2_selection": False, + "supports_initial_basis": False, + "description": "Two-stage: train NQS via VMC, sample, diagonalise.", + "pipeline_folder": "012_nqs_sqd", + }, + "nqs_skqd": { + "run_fn": run_nqs_skqd, + "config_cls": NQSSKQDConfig, + "iterative": False, + "has_krylov_expansion": True, + "has_ibm_solver": False, + "has_pt2_selection": False, + "supports_initial_basis": False, + "description": "Two-stage: train NQS, sample, Krylov expand, diagonalise.", + "pipeline_folder": "013_nqs_skqd", + }, + "hi_nqs_sqd": { + "run_fn": run_hi_nqs_sqd, + "config_cls": HINQSSQDConfig, + "iterative": True, + "has_krylov_expansion": False, + "has_ibm_solver": True, + "has_pt2_selection": True, + "supports_initial_basis": True, + "description": "Iterative HI loop: NQS sampling, batch diag, teacher feedback, optional PT2 selection.", + "pipeline_folder": "010_hi_nqs_sqd", + }, + "hi_nqs_skqd": { + "run_fn": run_hi_nqs_skqd, + "config_cls": HINQSSKQDConfig, + "iterative": True, + "has_krylov_expansion": True, + "has_ibm_solver": True, + "has_pt2_selection": False, + "supports_initial_basis": True, + "description": "Iterative HI loop: NQS sampling, Krylov expand, batch diag, teacher feedback.", + "pipeline_folder": "011_hi_nqs_skqd", + }, +} diff --git a/src/qvartools/methods/nqs/_shared.py b/src/qvartools/methods/nqs/_shared.py new file mode 100644 index 0000000..b4087d8 --- /dev/null +++ b/src/qvartools/methods/nqs/_shared.py @@ -0,0 +1,186 @@ +""" +_shared --- Shared helpers for NQS method runners +================================================== + +Internal utilities extracted from the four NQS method modules +(``nqs_sqd``, ``nqs_skqd``, ``hi_nqs_sqd``, ``hi_nqs_skqd``) to remove +duplication. + +Most extractions are behaviour-preserving. Scope notes: + +1. ``build_autoregressive_nqs`` is a direct lift of identical inline code + from all four method modules — no behaviour change. +2. ``validate_initial_basis`` is a direct lift of the identical validation + block from ``hi_nqs_sqd`` and ``hi_nqs_skqd`` — no behaviour change. +3. ``extract_orbital_counts`` applies the HI-method fall-back logic + (``mol_info`` → ``hamiltonian.integrals``) to ALL four runners. For + the HI methods (``hi_nqs_sqd``, ``hi_nqs_skqd``) this is a pure + refactor. For the simple methods (``nqs_sqd``, ``nqs_skqd``) this + fixes a pre-existing bug: they previously accessed + ``mol_info["n_orbitals"]`` directly, but ``get_molecule()`` does not + populate that key, so those runners were broken end-to-end. Routing + through this helper gives them the same fall-back semantics as the HI + methods and makes them actually runnable (verified by smoke test on + H2 via ``experiments/pipelines/012_nqs_sqd/default.py``). + +Functions +--------- +build_autoregressive_nqs + Build an :class:`AutoregressiveTransformer` NQS on a target device. +extract_orbital_counts + Extract ``(n_orb, n_alpha, n_beta, n_qubits)`` from ``mol_info`` + with fall-back to ``hamiltonian.integrals``. +validate_initial_basis + Validate a user-provided ``initial_basis`` tensor (shape, dtype, + binary values) and return a normalised long-tensor on the target + device. +""" + +from __future__ import annotations + +from typing import Any + +import torch + +from qvartools.nqs.transformer.autoregressive import AutoregressiveTransformer + +__all__ = [ + "build_autoregressive_nqs", + "extract_orbital_counts", + "validate_initial_basis", +] + + +def build_autoregressive_nqs( + n_orb: int, + n_alpha: int, + n_beta: int, + *, + embed_dim: int, + n_heads: int, + n_layers: int, + device: torch.device | str, +) -> AutoregressiveTransformer: + """Build an autoregressive transformer NQS on the target device. + + Parameters + ---------- + n_orb : int + Spatial orbitals per spin channel. + n_alpha, n_beta : int + Particle counts in each spin channel. + embed_dim : int + Transformer embedding dimension. + n_heads : int + Number of attention heads. + n_layers : int + Number of transformer layers per spin channel. + device : torch.device or str + Target device for the NQS parameters. + + Returns + ------- + AutoregressiveTransformer + The constructed NQS, moved to ``device``. + """ + return AutoregressiveTransformer( + n_orbitals=n_orb, + n_alpha=n_alpha, + n_beta=n_beta, + embed_dim=embed_dim, + n_heads=n_heads, + n_layers=n_layers, + ).to(device) + + +def extract_orbital_counts( + mol_info: dict[str, Any], + hamiltonian: Any, +) -> tuple[int, int, int, int]: + """Extract ``(n_orb, n_alpha, n_beta, n_qubits)`` from mol_info. + + Falls back to ``hamiltonian.integrals`` for any key missing from + ``mol_info``. Used by HI methods that accept partially-populated + ``mol_info`` dicts. + + Parameters + ---------- + mol_info : dict + Molecular metadata. May contain any subset of ``"n_orbitals"``, + ``"n_alpha"``, ``"n_beta"``, ``"n_qubits"``. + hamiltonian : Hamiltonian + Molecular Hamiltonian; expected to expose ``.integrals`` with + ``.n_orbitals``, ``.n_alpha``, ``.n_beta`` attributes when + ``mol_info`` is missing those keys. + + Returns + ------- + tuple of (int, int, int, int) + ``(n_orbitals, n_alpha, n_beta, n_qubits)``. + + Raises + ------ + ValueError + If ``n_orbitals``, ``n_alpha``, or ``n_beta`` cannot be resolved + from either ``mol_info`` or ``hamiltonian.integrals``. + """ + _integrals = getattr(hamiltonian, "integrals", None) + n_orb = mol_info.get("n_orbitals", _integrals.n_orbitals if _integrals else None) + n_alpha = mol_info.get("n_alpha", _integrals.n_alpha if _integrals else None) + n_beta = mol_info.get("n_beta", _integrals.n_beta if _integrals else None) + if n_orb is None or n_alpha is None or n_beta is None: + raise ValueError( + "n_orbitals, n_alpha, and n_beta must be provided via mol_info " + "or hamiltonian.integrals. Got: " + f"n_orbitals={n_orb}, n_alpha={n_alpha}, n_beta={n_beta}" + ) + n_qubits = mol_info.get("n_qubits", 2 * n_orb) + return int(n_orb), int(n_alpha), int(n_beta), int(n_qubits) + + +def validate_initial_basis( + initial_basis: torch.Tensor, + n_qubits: int, + *, + device: torch.device | str, +) -> torch.Tensor: + """Validate and normalise a user-supplied ``initial_basis`` tensor. + + Performs fail-fast checks on the raw input (dtype, shape, binary + values) before any cast, then returns a deduplicated long-tensor + on the target device. + + Parameters + ---------- + initial_basis : torch.Tensor + Pre-computed configurations to seed a cumulative basis. + n_qubits : int + Expected second-axis dimensionality. + device : torch.device or str + Target device for the returned tensor. + + Returns + ------- + torch.Tensor + Long-dtype tensor on ``device``, deduplicated along axis 0. + + Raises + ------ + ValueError + If ``initial_basis`` has floating-point or complex dtype, wrong + shape, or non-binary values. + """ + if initial_basis.is_floating_point() or initial_basis.is_complex(): + raise ValueError( + f"initial_basis must be integer or bool dtype (binary occupations), " + f"got {initial_basis.dtype}" + ) + if initial_basis.ndim != 2 or initial_basis.shape[1] != n_qubits: + raise ValueError( + f"initial_basis must have shape (n_configs, {n_qubits}), " + f"but got {tuple(initial_basis.shape)}" + ) + if not torch.all((initial_basis == 0) | (initial_basis == 1)): + raise ValueError("initial_basis must contain only binary values {0, 1}") + out = initial_basis.to(dtype=torch.long, device=device) + return torch.unique(out, dim=0) diff --git a/src/qvartools/methods/nqs/hi_nqs_skqd.py b/src/qvartools/methods/nqs/hi_nqs_skqd.py index ee6dbd5..7c1295f 100644 --- a/src/qvartools/methods/nqs/hi_nqs_skqd.py +++ b/src/qvartools/methods/nqs/hi_nqs_skqd.py @@ -36,6 +36,11 @@ ) from qvartools._utils.gpu.diagnostics import gpu_solve_fermion from qvartools.krylov.expansion.krylov_expand import expand_basis_via_connections +from qvartools.methods.nqs._shared import ( + build_autoregressive_nqs, + extract_orbital_counts, + validate_initial_basis, +) from qvartools.nqs.transformer.autoregressive import AutoregressiveTransformer from qvartools.solvers.solver import SolverResult @@ -236,20 +241,7 @@ def run_hi_nqs_skqd( """ cfg = config or HINQSSKQDConfig() - # Support mol_info with or without orbital counts (fall back to hamiltonian) - _integrals = getattr(hamiltonian, "integrals", None) - n_orb: int = mol_info.get( - "n_orbitals", _integrals.n_orbitals if _integrals else None - ) - n_alpha: int = mol_info.get("n_alpha", _integrals.n_alpha if _integrals else None) - n_beta: int = mol_info.get("n_beta", _integrals.n_beta if _integrals else None) - if n_orb is None or n_alpha is None or n_beta is None: - raise ValueError( - "n_orbitals, n_alpha, and n_beta must be provided via mol_info " - "or hamiltonian.integrals. Got: " - f"n_orbitals={n_orb}, n_alpha={n_alpha}, n_beta={n_beta}" - ) - n_qubits: int = mol_info.get("n_qubits", 2 * n_orb) + n_orb, n_alpha, n_beta, n_qubits = extract_orbital_counts(mol_info, hamiltonian) device = torch.device(cfg.device) logger.info( @@ -262,14 +254,15 @@ def run_hi_nqs_skqd( t_start = time.perf_counter() # --- Build NQS --- - nqs = AutoregressiveTransformer( - n_orbitals=n_orb, - n_alpha=n_alpha, - n_beta=n_beta, + nqs = build_autoregressive_nqs( + n_orb, + n_alpha, + n_beta, embed_dim=cfg.embed_dim, n_heads=cfg.n_heads, n_layers=cfg.n_layers, - ).to(device) + device=device, + ) nqs.eval() # --- Occupancies (uniform prior) --- @@ -278,21 +271,9 @@ def run_hi_nqs_skqd( # --- Cumulative basis (warm-start from initial_basis if provided) --- if initial_basis is not None: - # Validate raw input before any cast (fail-fast) - if initial_basis.is_floating_point() or initial_basis.is_complex(): - raise ValueError( - f"initial_basis must be integer or bool dtype (binary occupations), " - f"got {initial_basis.dtype}" - ) - if initial_basis.ndim != 2 or initial_basis.shape[1] != n_qubits: - raise ValueError( - f"initial_basis must have shape (n_configs, {n_qubits}), " - f"but got {tuple(initial_basis.shape)}" - ) - if not torch.all((initial_basis == 0) | (initial_basis == 1)): - raise ValueError("initial_basis must contain only binary values {0, 1}") - cumulative_basis = initial_basis.to(dtype=torch.long, device=device) - cumulative_basis = torch.unique(cumulative_basis, dim=0) + cumulative_basis = validate_initial_basis( + initial_basis, n_qubits, device=device + ) logger.info( "Warm-starting with %d initial basis configs", cumulative_basis.shape[0] ) diff --git a/src/qvartools/methods/nqs/hi_nqs_sqd.py b/src/qvartools/methods/nqs/hi_nqs_sqd.py index 225f3cf..a4b559f 100644 --- a/src/qvartools/methods/nqs/hi_nqs_sqd.py +++ b/src/qvartools/methods/nqs/hi_nqs_sqd.py @@ -39,6 +39,11 @@ compute_temperature, evict_by_coefficient, ) +from qvartools.methods.nqs._shared import ( + build_autoregressive_nqs, + extract_orbital_counts, + validate_initial_basis, +) from qvartools.nqs.transformer.autoregressive import AutoregressiveTransformer from qvartools.solvers.solver import SolverResult @@ -330,20 +335,7 @@ def run_hi_nqs_sqd( else: use_ibm = cfg.use_ibm_solver - # Support mol_info with or without orbital counts (fall back to hamiltonian) - _integrals = getattr(hamiltonian, "integrals", None) - n_orb: int = mol_info.get( - "n_orbitals", _integrals.n_orbitals if _integrals else None - ) - n_alpha: int = mol_info.get("n_alpha", _integrals.n_alpha if _integrals else None) - n_beta: int = mol_info.get("n_beta", _integrals.n_beta if _integrals else None) - if n_orb is None or n_alpha is None or n_beta is None: - raise ValueError( - "n_orbitals, n_alpha, and n_beta must be provided via mol_info " - "or hamiltonian.integrals. Got: " - f"n_orbitals={n_orb}, n_alpha={n_alpha}, n_beta={n_beta}" - ) - n_qubits: int = mol_info.get("n_qubits", 2 * n_orb) + n_orb, n_alpha, n_beta, n_qubits = extract_orbital_counts(mol_info, hamiltonian) device = torch.device(cfg.device) logger.info( @@ -356,14 +348,15 @@ def run_hi_nqs_sqd( t_start = time.perf_counter() # --- Build NQS --- - nqs = AutoregressiveTransformer( - n_orbitals=n_orb, - n_alpha=n_alpha, - n_beta=n_beta, + nqs = build_autoregressive_nqs( + n_orb, + n_alpha, + n_beta, embed_dim=cfg.embed_dim, n_heads=cfg.n_heads, n_layers=cfg.n_layers, - ).to(device) + device=device, + ) nqs.eval() # --- Occupancies (uniform prior) --- @@ -372,21 +365,9 @@ def run_hi_nqs_sqd( # --- Cumulative basis (warm-start from initial_basis if provided) --- if initial_basis is not None: - # Validate raw input before any cast (fail-fast) - if initial_basis.is_floating_point() or initial_basis.is_complex(): - raise ValueError( - f"initial_basis must be integer or bool dtype (binary occupations), " - f"got {initial_basis.dtype}" - ) - if initial_basis.ndim != 2 or initial_basis.shape[1] != n_qubits: - raise ValueError( - f"initial_basis must have shape (n_configs, {n_qubits}), " - f"but got {tuple(initial_basis.shape)}" - ) - if not torch.all((initial_basis == 0) | (initial_basis == 1)): - raise ValueError("initial_basis must contain only binary values {0, 1}") - cumulative_basis = initial_basis.to(dtype=torch.long, device=device) - cumulative_basis = torch.unique(cumulative_basis, dim=0) + cumulative_basis = validate_initial_basis( + initial_basis, n_qubits, device=device + ) logger.info( "Warm-starting with %d initial basis configs", cumulative_basis.shape[0] ) diff --git a/src/qvartools/methods/nqs/nqs_skqd.py b/src/qvartools/methods/nqs/nqs_skqd.py index 365e242..448fe6e 100644 --- a/src/qvartools/methods/nqs/nqs_skqd.py +++ b/src/qvartools/methods/nqs/nqs_skqd.py @@ -26,7 +26,10 @@ from qvartools._utils.gpu.diagnostics import gpu_solve_fermion from qvartools.krylov.expansion.krylov_expand import expand_basis_via_connections -from qvartools.nqs.transformer.autoregressive import AutoregressiveTransformer +from qvartools.methods.nqs._shared import ( + build_autoregressive_nqs, + extract_orbital_counts, +) from qvartools.solvers.solver import SolverResult __all__ = [ @@ -120,10 +123,7 @@ def run_nqs_skqd( """ cfg = config or NQSSKQDConfig() - n_orb: int = mol_info["n_orbitals"] - n_alpha: int = mol_info["n_alpha"] - n_beta: int = mol_info["n_beta"] - n_qubits: int = mol_info["n_qubits"] + n_orb, n_alpha, n_beta, n_qubits = extract_orbital_counts(mol_info, hamiltonian) device = torch.device(cfg.device) logger.info("run_nqs_skqd: %d orbitals, %d alpha, %d beta", n_orb, n_alpha, n_beta) @@ -131,14 +131,15 @@ def run_nqs_skqd( t_start = time.perf_counter() # --- Build NQS --- - nqs = AutoregressiveTransformer( - n_orbitals=n_orb, - n_alpha=n_alpha, - n_beta=n_beta, + nqs = build_autoregressive_nqs( + n_orb, + n_alpha, + n_beta, embed_dim=cfg.embed_dim, n_heads=cfg.n_heads, n_layers=cfg.n_layers, - ).to(device) + device=device, + ) # --- Stage 1: VMC pre-training --- optimiser = torch.optim.Adam(nqs.parameters(), lr=cfg.nqs_lr) diff --git a/src/qvartools/methods/nqs/nqs_sqd.py b/src/qvartools/methods/nqs/nqs_sqd.py index 372460a..33c07e9 100644 --- a/src/qvartools/methods/nqs/nqs_sqd.py +++ b/src/qvartools/methods/nqs/nqs_sqd.py @@ -25,7 +25,10 @@ import torch from qvartools._utils.gpu.diagnostics import gpu_solve_fermion -from qvartools.nqs.transformer.autoregressive import AutoregressiveTransformer +from qvartools.methods.nqs._shared import ( + build_autoregressive_nqs, + extract_orbital_counts, +) from qvartools.solvers.solver import SolverResult __all__ = [ @@ -113,10 +116,7 @@ def run_nqs_sqd( """ cfg = config or NQSSQDConfig() - n_orb: int = mol_info["n_orbitals"] - n_alpha: int = mol_info["n_alpha"] - n_beta: int = mol_info["n_beta"] - n_qubits: int = mol_info["n_qubits"] + n_orb, n_alpha, n_beta, n_qubits = extract_orbital_counts(mol_info, hamiltonian) device = torch.device(cfg.device) logger.info("run_nqs_sqd: %d orbitals, %d alpha, %d beta", n_orb, n_alpha, n_beta) @@ -124,14 +124,15 @@ def run_nqs_sqd( t_start = time.perf_counter() # --- Build NQS --- - nqs = AutoregressiveTransformer( - n_orbitals=n_orb, - n_alpha=n_alpha, - n_beta=n_beta, + nqs = build_autoregressive_nqs( + n_orb, + n_alpha, + n_beta, embed_dim=cfg.embed_dim, n_heads=cfg.n_heads, n_layers=cfg.n_layers, - ).to(device) + device=device, + ) # --- Stage 1: VMC pre-training --- optimiser = torch.optim.Adam(nqs.parameters(), lr=cfg.nqs_lr) diff --git a/tests/test_methods/test_initial_basis.py b/tests/test_methods/test_initial_basis.py index 7c832f9..ea19733 100644 --- a/tests/test_methods/test_initial_basis.py +++ b/tests/test_methods/test_initial_basis.py @@ -97,6 +97,7 @@ def _make_initial_basis(n_configs=3): class TestRunHiNqsSqdInitialBasis: """Test initial_basis kwarg for run_hi_nqs_sqd.""" + @patch("qvartools.methods.nqs.hi_nqs_sqd._IBM_SQD_AVAILABLE", False) @patch("qvartools.methods.nqs.hi_nqs_sqd.gpu_solve_fermion") def test_accepts_initial_basis_none( self, mock_solver, mol_info, minimal_config_sqd @@ -116,6 +117,7 @@ def test_accepts_initial_basis_none( assert isinstance(result, SolverResult) assert result.method == "HI+NQS+SQD" + @patch("qvartools.methods.nqs.hi_nqs_sqd._IBM_SQD_AVAILABLE", False) @patch("qvartools.methods.nqs.hi_nqs_sqd.gpu_solve_fermion") def test_accepts_initial_basis_tensor( self, mock_solver, mol_info, minimal_config_sqd @@ -137,6 +139,7 @@ def test_accepts_initial_basis_tensor( # With warm-start, final basis should be non-empty assert result.metadata["final_basis_size"] > 0 + @patch("qvartools.methods.nqs.hi_nqs_sqd._IBM_SQD_AVAILABLE", False) @patch("qvartools.methods.nqs.hi_nqs_sqd.gpu_solve_fermion") def test_initial_basis_deduplicates( self, mock_solver, mol_info, minimal_config_sqd diff --git a/tests/test_run_all_pipelines.py b/tests/test_run_all_pipelines.py index 8763413..ccb8b6f 100644 --- a/tests/test_run_all_pipelines.py +++ b/tests/test_run_all_pipelines.py @@ -18,13 +18,13 @@ def test_vqe_pipelines_are_registered() -> None: scripts = {script for _, script, _, _ in rap.PIPELINES} - assert "09_vqe/vqe_uccsd.py" in scripts - assert "09_vqe/vqe_adapt.py" in scripts + assert "009_vqe/vqe_uccsd.py" in scripts + assert "009_vqe/vqe_adapt.py" in scripts def test_vqe_pipeline_scripts_exist() -> None: - assert (PIPELINES_DIR / "09_vqe" / "vqe_uccsd.py").is_file() - assert (PIPELINES_DIR / "09_vqe" / "vqe_adapt.py").is_file() + assert (PIPELINES_DIR / "009_vqe" / "vqe_uccsd.py").is_file() + assert (PIPELINES_DIR / "009_vqe" / "vqe_adapt.py").is_file() def test_skip_quantum_filters_vqe_pipelines() -> None: